55 GiB/s FizzBuzz (2021)
(codegolf.stackexchange.com)- Code Golf Stack Exchange का High throughput Fizz Buzz एक performance experiment है, जिसमें यह मुकाबला होता है कि FizzBuzz output को कितनी तेज़ी से generate करके pipe में बहाया जा सकता है, और स्कोर
<program> | pv > /dev/nullthroughput से तय होता है - वैध output के लिए साधारण ASCII,
\nline break, बिल्कुल सही Fizz/Buzz/FizzBuzz, कम-से-कम2^58तक या व्यावहारिक रूप से अनंत output ज़रूरी है, और सिर्फpvमें दिखने वाली null byte trick प्रतिबंधित है - शीर्ष implementations
% 3,% 5,printfloops को छोड़कर 15-line pattern unrolling, fixed-width number handling, बड़े buffers, parallelization, और zero-copy output से bottleneck कम करती हैं - दिखाई गई x86-64+AVX2 assembly answer Linux और AVX2 को मानकर
vmsplice, L2 cache, huge page, और FizzBuzz bytecode interpreter को मिलाकर लगभग 61GiB/s दर्ज करती है - नतीजे CPU, pipe buffer,
pvversion, CPU affinity, mitigation settings से काफी बदलते हैं, और प्रश्नकर्ता की मशीन के हिसाब से सर्वोच्च स्कोर David Frank के C++ implementation का लगभग 1.7Tb/s बताया गया है
FizzBuzz से throughput की सीमा तक धक्का
- मूल बात FizzBuzz समस्या नहीं है, बल्कि यह देखना है कि बहुत सरल text generation में CPU computation, memory copy, pipe I/O, kernel boundary में से bottleneck सबसे पहले कहाँ आता है
- baseline के तौर पर एक naive C implementation
% 3,% 5,printfका उपयोग करती है और औसत मशीन पर लगभग 170MiB/s देती है - प्रश्नकर्ता ने उसी मशीन पर 3GiB/s से ऊपर की implementations पहले देखी थीं और चाहता था कि community throughput की इससे ऊँची सीमा तलाशे
- स्कोर प्रश्नकर्ता के desktop पर मापा गया
- AMD 5950x, 16C/32T
- 64GB 3200MHz RAM
- CPU mitigations disabled
- भाषा-वार तालिका में asm 60.8GiB/s, C 20.9GiB/s, Julia 15.5GiB/s, Go 6.8GiB/s, Java 5.8GiB/s, Rust 3.4GiB/s, Ruby 1.7GiB/s, Python 0.5GiB/s आदि शामिल हैं
- ais523 का x86-64+AVX2 assembly answer single-thread maximum performance को लक्ष्य बनाता है, और लेखक की मशीन पर लगभग 31GiB/s तथा प्रश्नकर्ता के संकलन में लगभग 61GiB/s दर्ज करता है
- David Frank की C++ implementation ने प्रश्न के मुख्य पाठ के अनुसार मौजूदा सर्वोच्च स्कोर के रूप में लगभग 1.7 Terrabit/s दर्ज किया, जबकि एक अलग C++20 implementation ने AMD Ryzen 9 7700X पर 283GB/s देने का दावा किया
Output rules और benchmark conditions
- वैध output की शर्तें कड़ी हैं
- output बिल्कुल सही FizzBuzz होना चाहिए
- हर ASCII character 1 byte होना चाहिए
- line break के लिए सिर्फ
\nमान्य है \r\nमान्य नहीं है- output कम-से-कम
2^58जैसी बहुत बड़ी संख्या तक जारी रहना चाहिए
- benchmark का तरीका भी throughput पर असर डालता है
- चर्चा में
pvऔर Linux pipe buffer को डिफ़ॉल्ट रूप से 64K buffer इस्तेमाल करने वाला बताया गया है - CPU sibling core placement के अनुसार generator program और consumer program के बीच L2 cache path बदल सकता है
tasksetसे CPU placement को force करके तुलना की जा सकती है
- चर्चा में
assembly implementation की optimization तकनीक
- assembly implementation का केंद्र calculation से ज़्यादा output copy cost घटाने पर है
writeइस्तेमाल करने पर user space से kernel space तक copy का खर्च बड़ा होता है- लेखक लिखता है कि
write-आधारित रूप में बदलने पर performance पाँचवें हिस्से तक गिर जाती है vmspliceसे pipe को program के buffer को reference करने दिया जाता है, जिससे copy कम होती है
- FizzBuzz calculation को 3 चरणों में बाँटा गया है
- पहला चरण शुरुआती strings को hardcode करता है
- दूसरा चरण 2 से 5 digit numbers को अपेक्षाकृत सीधे AVX2 routine से संभालता है
- तीसरा चरण 6 से 18 digit range को FizzBuzz bytecode interpreter से संभालता है
- तीसरे चरण का मुख्य loop हर 4 clock cycles पर 64 bytes output का लक्ष्य रखता है
- bytecode का 1 byte, output का 1 byte बनाता है
- 32-byte bytecode load करके
vpshufb,vpsubbआदि से 32 bytes output बनाई जाती है - line numbers को approximate value और bytecode correction से संभाला जाता है, ताकि हर line पर सामान्य number conversion न करनी पड़े
- यह implementation platform-dependent है
- अपेक्षाकृत नया Linux चाहिए
- AVX2 support वाला x86-64 processor चाहिए
- अगर standard output pipe नहीं है तो यह startup पर error देता है
- बताया गया है कि
spliceशामिल होने वाली pipe configuration में कभी-कभी गलत output आ सकता है
दूसरी भाषाओं की implementations में साझा रणनीतियाँ
- C, Go, Java, Rust, Python, Julia, Ruby, C# implementations भी optimization की मिलती-जुलती दिशा अपनाती हैं
- 15-line FizzBuzz pattern को unroll करती हैं
- number string conversion की संख्या घटाती हैं
- बड़े buffer में इकट्ठा करके लिखती हैं
- threads या goroutine से generation work को parallelize करती हैं
- output order बनाए रखने के लिए barrier, channel, queue, mutex का उपयोग करती हैं
- इस नतीजे को साधारण “language performance” तुलना मानना कठिन है
pvversion, pipe size,vmspliceavailability, CPU affinity, huge page, compiler optimization,memcpyinline होने या न होने से throughput बहुत बदलती है- कुछ answers के comments में कहा गया है कि वे किसी खास मशीन पर तेज़ थे, लेकिन दूसरे environments में वही आँकड़े दोहराए नहीं जा सके
शामिल न किया गया दायरा
- input note में संकेत है कि कुछ source chunks लंबाई और लागत सीमाओं के कारण छोड़े गए थे, इसलिए सभी 46 answers और हर code/comment को पूरी तरह शामिल नहीं किया गया है
- छोड़े गए हिस्सों में कुछ भाषा-विशिष्ट submissions, लंबे code bodies, विस्तृत tuning logs, और comment discussions शामिल हो सकते हैं
1 टिप्पणियां
Hacker News की राय
सबसे प्रभावशाली बात यह लगी कि Linux एक प्रोग्राम से दूसरे प्रोग्राम में pipe किए गए डेटा को सिर्फ L2 cache में ही बनाए रख सकता है और main memory को छुए बिना काम कर सकता है
आम Linux kernel के कई हिस्सों का एक साथ मिलकर इस तरह का fast path संभव बनाना वाकई कमाल की system architecture design है
जिज्ञासा है कि क्या Mac OSX के Mach ports या Windows के Named Pipes में भी ऐसा संभव है
“ais523 - high effort answers” नाम के अनुरूप, इस यूज़र ने प्रोग्राम चला न पाने वाले व्यक्ति के लिए भी काफ़ी मेहनत से भरी टिप्पणी छोड़ी, और निष्कर्ष यह था
“शायद प्रोग्राम किसी तरह ASLR enabled स्थिति में compile हो गया। इस स्थिति में dynamic linker BSS segment की 4MiB alignment का सम्मान नहीं करता, इसलिए मेरी
.alignनिर्देशिका प्रभावी रूप से अनदेखी हो जाती है, और शायद वही bug का कारण है”यह पोस्ट जब भी फिर से सामने आती है, मैं इस टिप्पणी पर हँस पड़ता हूँ
“@chx: मेरे पास पहले से master’s thesis है। यह उससे भी ज़्यादा कठिन था। – ais523 - high effort answers Oct 29, 2021 at 1:17”
Rust, Python3, और C में optimization के बिना simple implementation करके देखा। सिर्फ साधारण if/else/while और standard output पर print इस्तेमाल किया
Rust -> 23.2MiB/s
Python3 -> 28.6MiB/s
C -> 238MiB/s
किसी को पता है कि Rust performance Python3 के आसपास क्यों है? मुझे लगा था यह C के कहीं ज़्यादा करीब होगा
C जैसी performance पाने के लिए संभव है कि इस lock को सीधे handle करना पड़े
let mut lock = stdout().lock();write!(lock, "hello world").unwrap();और standard output के buffer size को भी C के बराबर करना होगा
https://ismailmaj.github.io/tinkering-with-fizz-buzz-and-con...
1ms delay के साथ नंबर गिनने वाले C प्रोग्राम में दूसरा कॉलम पिछली
read()के बाद बीता समय है$ ./out | rtss4.7ms 4.7ms | 14.7ms | 24.7ms | 34.7ms | 44.8ms exit status: 0देखा जा सकता है कि सब कुछ एक साथ लिखा गया। Terminal allocate करने पर हर लाइन अलग-अलग आती है
$ rtss --pty ./out0.8ms 0.8ms | 11.9ms 1.1ms | 23.0ms 1.1ms | 34.1ms 1.1ms | 44.3ms exit status: 0Rust में output के लिए ऐसा adaptive व्यवहार नहीं है, इसलिए terminal हो या न हो, यह हमेशा दूसरे नतीजे की तरह व्यवहार करता है
तकनीकी रूप से, standard output को हमेशा LineWriter(https://doc.rust-lang.org/std/io/struct.LineWriter.html) में wrap किया जाता है, और newline वाला write दिखते ही हमेशा flush हो जाता है। Throughput को अधिकतम करने के लिए इसे BufWriter से wrap करके कई लाइनों को साथ लिखना चाहिए
--releaseसे, और C को-O3से compile करना चाहिएअगर सब कुछ assembly में लिखा गया होता तो यह कितना तेज़ होता, यह जानने की जिज्ञासा होती है
audio development में DSP code को assembly में लिखना बहुत आम है
इसे संभव बनाने के लिए बेहद गहन algorithm research, Linux system calls की गहरी समझ, और platform-specific optimization का मेल होना चाहिए। लेखक Alex Smith के अपने शब्दों में:
“@chx: मेरे पास पहले से एक master's thesis है. यह उससे भी ज़्यादा कठिन था.”
यह उस चीज़ से बिल्कुल अलग स्तर का काम है जिसे बस “assembly में कर दो” कहकर बनाया जा सके
“ठीक है। मैं University of Washington में पढ़ता था, फिर Geoworks नाम की एक कंपनी में नौकरी कर ली और 5 साल तक assembly language programming की। हम Geoworker लोग पूरा operating system, libraries, drivers, apps—यानी पूरा desktop operating system—assembly में लिखते थे। वह 8086 assembly थी! और वह कोई अच्छी assembly भी नहीं थी! उसमें सिर्फ चार registers थे! अगर 386 तक गिनें तो si register भी था। बहुत भयानक था।
सच कहूँ तो हमें यह काफ़ी पसंद भी था। क्योंकि वह object-oriented assembly थी। इंसान अपने-आप को क्या-क्या पसंद करवाने के लिए मना सकता है, यह हैरान करने वाली बात है, और यही इस पूरी कहानी की असली विडंबना है। हमारे लिए C++ रोमन शैली के पतन की पराकाष्ठा था। जैसे ज़्यादा खाने के लिए उल्टी करने चले जाना। उनके पास IF था! हमारे पास jump CX zero था! उनके पास ‘objects’ थे। हमारे पास भी थे, लेकिन उनके लिए उनके पास syntax था। यह बहुत कमज़ोर लगता था। और उस समय हमें पता था कि हम किसी भी compiler से तेज़ code बना सकते हैं, और हम सचमुच बनाते भी थे!
तो फिर क्या हुआ? कंपनी दिवालिया हो गई। क्यों? शायद इस बात पर मैं हर Geoworker से असहमत हूँ। मुझे पता है कि शायद यह मानने वाला मैं अकेला ही हूँ। लेकिन वजह यह थी कि हमने 8086 assembly language की 1.5 करोड़ lines लिख डाली थीं। हमारे पास बहुत अच्छे tools थे, world-class tools, यक़ीन मानिए। ऐसे tools ज़रूरी भी होते हैं। लेकिन एक समय ऐसा आता है…
समस्या यह है: कल्पना कीजिए कि एक चींटी garage के फ़र्श पर सीधी रेखा में चलकर पार जाना चाहती है। वह सीधा नहीं जा सकती। हमें ऊपर से व्यापक नज़र मिलती है, इसलिए हम यह जानते हैं। हम देखते हैं कि चींटी इधर-उधर घूमती है, उस पत्थर के हिसाब से local optimization करती है, फिर उधर मुड़ती है।
जब हम विशाल assembly language systems लिख रहे थे, तब हम भी बिल्कुल यही कर रहे थे। आखिर में Microsoft ने हमसे कहीं तेज़ mobile device platform निकाल दिया। मैं debugger लेकर अंदर गया और कहा, ‘यह क्या है? ऐसा क्यों हो रहा है? rendering बहुत धीमी है, अटक रही है।’ अंदर जाकर देखा तो हर बार screen refresh होने पर कोई title bar 140 बार render हो रही थी। और सिर्फ title bar ही नहीं—हर चीज़ कई-कई बार call हो रही थी।
क्योंकि हम अब यह देख ही नहीं पा रहे थे कि system असल में कैसे काम कर रहा है!
छोटे systems सिर्फ optimize करना आसान नहीं होते, वे सच में optimize किए जा सकते हैं। यानी global optimization संभव होती है.”
http://steve-yegge.blogspot.com/2008/05/dynamic-languages-st...
आज की websites और software का लगभग 99% सिर्फ application performance पर थोड़ा ध्यान देकर, बुनियादी बदलावों से ही कम-से-कम 50% speed improvement हासिल कर सकता है। जैसे सही caching, asset optimization, फूली हुई third-party libraries को उसी काम के लिए native calls से बदलना, और server व database की सही configuration
यह भी लगता है कि कुछ सालों में AI repositories के लिए one-click optimization दे सकता है, जो best practices लागू कर दे या मूल code को तेज़ assembly में फिर से लिख दे
resource constraints फ़ोकस को स्पष्ट बना सकते हैं
यह प्रयोग मज़ेदार और जानकारीपूर्ण तो है, लेकिन इसमें थोड़ी खामी दिखती है। यह इस बात का मूल्यांकन कम करता है कि कोई जटिल समस्या कितनी तेज़ी से हल होती है, और ज़्यादा इस सहायक समस्या को टेस्ट करता है कि एक प्रोसेस से मेमोरी निकालकर दूसरे प्रोसेस को कितनी दक्षता से सौंपी जाती है
इसलिए ऐसा लगता है कि दूसरा प्रोसेस लगातार console या file में लिख रहा है, लेकिन तकनीकी रूप से ऐसा नहीं है।
pv >/dev/nullचलाना मूलतः लगभग कुछ भी न करने जैसा है, और write system call लगभग तुरंत return हो जाती हैvmspliceएक shared memory जैसी सुविधा है, जो एक प्रोसेस के buffer/memory तक दूसरे प्रोसेस को पहुँचने देती है। शुरुआती प्रतियोगिता की requirements शायद अस्पष्ट थीं, इसलिए यह नियमों के हिसाब से ठीक था या नहीं, यह स्पष्ट नहीं लगता“एक FizzBuzz program लिखिए। उसे चलाइए। output को
| pv > /dev/nullमें pipe कीजिए। throughput जितना ज़्यादा, उतना बेहतर।”“Program output बिल्कुल वैध fizzbuzz होना चाहिए। वैध output के बीच null byte घुसाकर cheat नहीं कर सकते। जैसे null byte जो console पर नहीं दिखते लेकिन pv throughput में गिने जाते हैं।”
और
vmsplice(2)वास्तव में standard output pipe में एक byte stream बनाता है, जिसेpv(1)/dev/nullमें splice कर सकता है याcat(1)terminal पर copy कर सकता हैसिर्फ इसी submission ने
vmsplice(2)का इस्तेमाल नहीं किया। दूसरे submitters ने भी पता लगाया कि यह कोई जादुई समाधान नहीं है। I/O barrier पार करने के बाद भी output pages को जितना संभव हो उतनी तेज़ी से बनाना बाकी रहता हैज़्यादातर code की bottleneck memory और I/O में होती है। जटिल समस्याएँ भी आम तौर पर data को एक जगह से दूसरी जगह ले जाने की गति से अटकती हैं; data की computation से अटकना दुर्लभ है। GPU assembly optimize करते हुए दिन बिताने वाले व्यक्ति के रूप में भी, जिन दुर्लभ मामलों में computation bottleneck होती है, उसे optimize करने के बाद bottleneck फिर memory बन जाती है
उदाहरण के लिए, एक चतुर bit representation है जो decimal carry को स्वाभाविक रूप से होने देती है
शुरुआती प्रतियोगिता requirements भी इस बिंदु पर ख़ास अस्पष्ट नहीं थीं। उसमें साफ़ कहा गया था कि throughput को
| pv > /dev/nullसे मापना है, और यह भी कहा गया था:“Architecture-specific optimization / assembly भी स्वीकार्य है। यह कोई असली competition नहीं है। हम बस लोगों को fizz buzz को उसकी सीमा तक धकेलते देखना चाहते हैं, भले ही वह सिर्फ़ किसी special environment/platform पर ही काम करे।”
memory I/O और file I/O को अच्छी performance के साथ handle करना समझना हर program और programmer के लिए प्रासंगिक कौशल है
इसमें
fizzbuzz.Sनाम से save करने को कहा गया है, तो मैं सोच रहा हूँ कि extension .S और .s में क्या फ़र्क हैmanual page के अनुसार:
file.sassembler code
file.Sfile.sxpreprocessed assembler code
.S) या नहीं (.s)आधुनिक toolchain में अब भी कोई फ़र्क है या नहीं, यह मुझे पक्का नहीं मालूम
.Sमानव द्वारा सीधे लिखी गई assembly file के लिए होता है, आम तौर पर git में tracked file, और.smachine-generated assembly के लिए, जिसे ज़रूरत पड़ने पर overwrite किया जा सके.Sको overwrite नहीं करते, लेकिन अगर आप assembly generate करने को कहें (जैसेgcc -S xyz.c) तो.soverwrite कर देते हैंपिछली पोस्टें:
https://news.ycombinator.com/item?id=29031488
https://news.ycombinator.com/item?id=29413656
मैंने पहले इसे 55 GiB/s FritzBox पढ़ लिया था। FritzBox यूरोप के जर्मन-भाषी क्षेत्रों में लोकप्रिय router है
मेरे ISP ने भी पिछले हफ़्ते ट्वीट किया था कि 60 GiB/s support वाला OPNSense box[1] जल्द उपलब्ध होगा
[1] https://twitter.com/init7/status/1674920410889043973
मैंने एक USB WiFi adapter भी लगाया हुआ है, ताकि cable कटने पर कुछ VLAN fail over हो जाएँ और phone के ज़रिए काम के लिए ज़रूरी connectivity बनी रहे
यह सस्ता नहीं है, लेकिन अगर आप OPNSense project को आर्थिक रूप से support करते हुए सही hardware चाहते हैं, तो Deciso hardware के ख़िलाफ़ तर्क करना मुश्किल है। यह power-efficient है, durability-focused components इस्तेमाल करता है, और बस ठीक से काम करता है
commercial lineup को और शक्तिशाली होते देखना अच्छा लगता है