- जनवरी 2024 के पूरे महीने चली One Billion Row Challenge(1BRC) एक performance challenge थी, जिसमें 1 अरब पंक्तियों वाली text file को process करके यह देखा गया कि Java कितनी तेज़ हो सकती है
- input
station;temperatureformat का साधारण text है, लेकिन हर observation station के लिए न्यूनतम·औसत·अधिकतम तापमान calculate करके नाम के क्रम में बिल्कुल सही output देना होता है - implementation में सिर्फ Java की अनुमति है; SDKMan distributions और openjdk.net Early Access builds इस्तेमाल किए जा सकते हैं, लेकिन external dependencies प्रतिबंधित हैं
- प्रतिभागी GitHub के 1brc repository में pull request के जरिए submit करते हैं, और दिए गए baseline implementation से answer format और performance की तुलना कर सकते हैं
- evaluation समान Hetzner Cloud CCX33 environment में 5 बार run करने के बाद सबसे कम और सबसे ज्यादा records हटाकर बाकी 3 runs के average से leaderboard ranking तय करता है
1 अरब पंक्तियों को सबसे तेज़ aggregate करने वाला Java task
- One Billion Row Challenge 1 जनवरी 2024 से 31 जनवरी 2024 तक चली Java performance challenge थी
- प्रतिभागी एक Java program लिखते हैं जो text file से temperature measurements पढ़कर प्रत्येक weather observation station के लिए न्यूनतम·औसत·अधिकतम तापमान calculate करता है
- difficulty का मुख्य बिंदु यह है कि input file में 1,000,000,000 पंक्तियां हैं
- input एक simple structure है, जिसमें एक line में एक measurement होता है
- उदाहरण:
Hamburg;12.0 - उदाहरण:
Bulawayo;8.9 - उदाहरण:
Palembang;38.8
- उदाहरण:
- output में observation station names को alphabetical order में sort करना और हर station के
min/mean/maxvalues दिखाने होते हैं- उदाहरण:
{Abha=5.0/18.0/27.4, Abidjan=15.7/26.0/34.1, ...}
- उदाहरण:
Submission rules और execution environment
- लक्ष्य वही काम करने वाला सबसे तेज़ Java implementation बनाना है
- optimization में virtual threads, Vector API और SIMD, GC optimization, AOT compilation आदि का उपयोग किया जा सकता है
- basic rules इस प्रकार हैं
- submission Java में लिखा होना चाहिए
- SDKMan द्वारा प्रदान किए गए Java distributions और openjdk.net के Early Access builds इस्तेमाल किए जा सकते हैं
- Valhalla जैसे OpenJDK projects के EA builds भी allowed हैं
- external dependencies इस्तेमाल नहीं की जा सकतीं
- प्रतिभागी 1brc repository clone करके README instructions के अनुसार implementation submit करते हैं
- baseline implementation comparison baseline और answer format check करने के लिए दिया गया है
- submission upstream repository में pull request खोलकर किया जाता है
Leaderboard calculation method और community sharing
- evaluation Hetzner Cloud CCX33 instance पर किया जाता है
- specification 8 dedicated vCPU, 32 GB RAM है
timeprogram से end-to-end execution time measure किया जाता है- हर submission लगातार 5 बार run किया जाता है
- सबसे धीमा run और सबसे तेज़ run exclude किए जाते हैं
- बचे हुए 3 runs के execution time का average उस submission का result बनता है
- results leaderboard में जोड़े जाते हैं
- optimization techniques पर चर्चा GitHub repository के discussion में जारी रहती है
- Java के अलावा दूसरी languages के implementations share करने के लिए Show & Tell भी उपलब्ध है, जहां Rust, Go, C++ आदि के 1BRC implementations share किए गए हैं
2 टिप्पणियां
Hacker News की राय
अभी सबसे बेहतर प्रदर्शन करने वाला दिख रहा समाधान [0] hash collision को ध्यान में नहीं रखता, इसलिए अगर dataset में अलग-अलग शहरों की संख्या काफी ज़्यादा हो, तो यह गलत परिणाम दे सकता है
सोच रहा हूँ कि कहीं मैं कुछ मिस तो नहीं कर रहा
[0] https://github.com/gunnarmorling/1brc/blob/main/src/main/jav...
फिलहाल उन entries को leaderboard से हटा दिया गया है, और दोनों लेखक अपनी submissions ठीक कर रहे हैं, इसलिए बाद में उन्हें फिर जोड़ा जाएगा
[0] https://twitter.com/mtopolnik/status/1742652716919251052
मुझे लगता है कि इस तरीके से पूरी चीज़ को 0.3 सेकंड के अंदर प्रोसेस किया जा सकता है
तापमान में दशमलव का सिर्फ एक अंक है, इसलिए सामान्य स्थिति में लगभग 400 values काफ़ी हैं, और place names भी लगभग 400 तक सीमित हैं, तो temperature × place name के लगभग 1.6 लाख combinations की lookup table बनाई जा सकती है
फिर एक state machine auto-generate की जा सकती है, जो इन 1.6 लाख combinations को, चाहे वे 4-byte register में किसी भी rotation position पर हों, hash table के unique bucket से map करे, और 32-bit state register में हर cycle state-transition table lookup तथा अगले 4 bytes के साथ XOR करे
पूरे data को memory speed पर scan करते हुए state के हिसाब से counters बढ़ाए जा सकते हैं, और state केवल 65K हैं इसलिए counters cache में आ जाएंगे
AVX512 के साथ, प्रति core ऐसी 32-bit state machines को 512 parallel में चलाया जा सकता है, इसलिए computation bottleneck नहीं होगा
जो बहुत ऊँचे/नीचे तापमान या अनजान place names valid buckets पर map नहीं होते, उन्हें slow code path पर भेजा जा सकता है, और min/max handling भी ऐसे ही escape के ज़रिए की जा सकती है, जो सिर्फ कुछ हज़ार बार होगा
मेरा मानना है कि यह तरीका सिर्फ एक AVX512 single core पर memory speed से चल सकता है, इसलिए इसे कई cores में बाँटने का खास फायदा नहीं होगा
ज़रूरत सिर्फ 400 entries वाली hash table, running min/average/max के 3 floating-point values, और average update के लिए एक count integer की है
नामों पर 16 bytes भी खर्च करें, तब भी सब कुछ 16KB के अंदर आ जाएगा
runtime पर I/O हावी होगा, और उसके बाद JSON parsing
ज़्यादातर modern x86 server chips प्रति clock 2 SIMD loads retire कर सकते हैं, इसलिए AVX2 के हिसाब से 1GHz पर लगभग 32GB/s संभव है, यानी per-core bandwidth को maximize करने के लिए AVX-512 अनिवार्य नहीं है
लेकिन अगर DRAM से पढ़ रहे हैं, तो bottleneck इससे बहुत पहले, आम तौर पर servers में 10~16GB/s के आसपास आ सकता है
जब तक ज़्यादातर data RAM तक spill हो रहा है, single-core throughput काफ़ी गिर जाएगा, और बड़े streaming workloads में multi-core parallelism लगभग हमेशा फ़ायदेमंद होती है
L3 cache से बहुत बड़ा memory block allocate करके, pages को पहले से fault-in कराकर, फिर tight loop में unrolled vector loads (AVX2/AVX-512) चलाकर इसे आसानी से verify किया जा सकता है
और state register को interpret कैसे करेंगे, यह भी सवाल है। input के 4 bytes के साथ XOR करने पर, किसी अनपेक्षित place name के लिए यह वास्तव में 4.7 अरब संभावित values में से कुछ भी बन सकता है
और अपेक्षित place name के मामले में भी, अगर वह 4 bytes से लंबा है, तो common prefix वाले दूसरे नामों से अलग करने के लिए क्या हर नाम के लिए कई states नहीं चाहिए होंगे?
नियम कहते हैं कि भले ही data generator station names के fixed set का उपयोग करे, कोई भी समाधान arbitrary UTF-8 station names पर काम करना चाहिए
सबसे धीमे और सबसे तेज़ run को हटाकर बाकी तीन का average लेने की बजाय, मेरा मानना है कि या तो दो सबसे धीमे runs हटाने चाहिए, या बस सबसे तेज़ value को मान लेना चाहिए
मुझे नहीं लगता कि अच्छे run result को फेंकने की कोई ठोस वजह है
लेकिन अगर program के अंदर non-determinism का थोड़ा भी कारण हो, जो सोचे से ज़्यादा आम है, तो best time representative न होने की पूरी संभावना है
इस बारे में https://tratt.net/laurie/blog/2019/minimum_times_tend_to_mis... एक अच्छी पोस्ट है
नियमों को सख्ती से मानने वाले नज़रिए से देखें तो पहली रन पर background daemon उठाकर पूरी फ़ाइल को मेमोरी में लोड करके pin कर देना, फिर बाद की runs को लगभग सिर्फ़ linear scan तक सीमित कर देना, और cache भी पहले से warm कर लेना आकर्षक लगता है
पहली रन में result पहले से precompute करना भी, नियमों की व्याख्या को कितना खींचा जाए इस पर निर्भर करते हुए, संभव लगता है; और संख्याओं को पहले से अधिक सघन format में parse करके रखना और बाद की runs में उन्हें सीधे cumulative sum की तरह पढ़ना भी संभव लग सकता है
यह प्रतियोगिता के उद्देश्य के बिल्कुल विपरीत है, लेकिन दिखने वाले नियमों के हिसाब से शायद स्पष्ट रूप से मना नहीं है
अगर precomputation पसंद नहीं है, तो input को पहले से sort करना, pre-parse करना, या compression·sorting·sorted memory layout जैसी तरकीबें भी संभव हैं
चरम स्थिति में
calculate_timeस्क्रिप्ट को patch करके अपने लिए 0 सेकंड और प्रतियोगी के लिए 9999 लौटाया जा सकता हैinput पढ़े बिना answer को एक लाइन में hardcode करने से लेकर, फ़ाइल की सामग्री नहीं जानते मानकर process करने तक, precomputation का gray area लगभग 1 अरब चरणों जितना बड़ा है
यह एक ऐसी प्रतियोगिता बन सकती है जिसमें तय करना पड़े कि कौन-सा precomputation निष्पक्ष है और कौन-सा नहीं
इसी वजह से machine learning प्रतियोगिताएँ प्रतिभागियों को final data नहीं दिखातीं
लिखा है कि computation application run के समय होनी चाहिए, और build समय पर measurement फ़ाइल को process करके result को binary में bake नहीं करना चाहिए
मुझे लगता है यह बस disk speed से बँधी हुई समस्या नहीं है क्या। SIMD या multithreading जैसी optimizations का कितना मतलब होगा, इस पर संदेह है
अलग-अलग stations की संख्या और hash lookup के तरीके पर यह निर्भर करेगा, लेकिन I/O की तुलना में यह मापने लायक असर डालेगा या नहीं, इस पर शंका है
आधुनिक hardware को ध्यान में रखकर डिज़ाइन किए गए systems इसी बात का फायदा उठाते हैं, और जहाँ मैं काम करता हूँ, redpanda.com, वह भी ऐसा ही एक उदाहरण है
parsing computation time का बड़ा हिस्सा लेती है, और delimiter ढूँढने की SWAR जैसी SIMD techniques मददगार हो सकती हैं
अगर ऐसे algorithms की साफ़-सुथरी implementation देखनी हो तो Stringzilla अच्छा है: https://github.com/ashvardanian/StringZilla
पहली run के बाद फ़ाइल पूरी तरह मेमोरी में cache हो जाती है, इस बिंदु पर यहाँ जवाब दिया गया है: https://news.ycombinator.com/item?id=38864034
आम तौर पर servers में ऐसे 15 SSD लगाने के लिए पर्याप्त PCIe lanes होते हैं, इसलिए server I/O bandwidth मेमोरी bandwidth के काफ़ी करीब पहुँच जाती है
अधिक महँगे servers में PCIe 5.0 जैसी तेज़ lanes और उनकी अधिक संख्या होती है
यह फ़ाइल 1 अरब rows की है और compress होने पर लगभग 1GB की है, और पहली discarded run के बाद मेमोरी में आ जाती है, इसलिए इस scenario में I/O bandwidth महत्वपूर्ण नहीं है
GitHub repository में इसे uncompressed 12GB बताया गया है, और यह भी इसी बात की पुष्टि करता है कि I/O bandwidth यहाँ महत्वपूर्ण नहीं है
मुख्य बात यह है कि disk का bottleneck होना दुर्लभ है
उदाहरण के लिए Linux पर ext2 इस्तेमाल हो तो पहली run के बाद पूरी फ़ाइल cache होने की संभावना ज़्यादा है, लेकिन ZFS पर ऐसा ज़रूरी नहीं
तब संख्या के digit low place से high place के क्रम में मिलेंगे, फिर delimiter और string आएँगे, और EOF या newline मिलने तक आगे बढ़ा जा सकता है
नियमों के अनुसार submission को हर input पर सही काम करना चाहिए, लेकिन ऐसा लगता है कि
create_measurements.shसे बनने वाले खास input के लिए tune करना allowed है, और शायद अपेक्षित भीउदाहरण के लिए दिए गए station set के लिए अनुकूलित perfect hash function इस्तेमाल करने वाली submission की कल्पना की जा सकती है
इससे overfitting optimization रोकी जा सकेगी
127 से बड़े byte multi-byte UTF-8 characters को दर्शाते हैं
मज़े के लिए awk बनाम Java speed comparison किया गया
यह
awk -F';'से station के हिसाब से sum, count, min, max जमा करता है और END में average निकालकर प्रिंट करने वाला script हैइसमें
file_fdwसे CSV फ़ाइल को external table बनाकरGROUP BY station_nameके साथMIN,AVG,MAXनिकाले जाते हैंclickhouse localमेंfile('measurements.txt', 'CSV', 'station String, t Float32')पढ़कर station के हिसाब सेmin,max,avggroup किया जाता है, औरmax_threads = 8के साथ चलाया जाता हैज़्यादातर समय फ़ाइल parsing में जाता है
sumvariable काफ़ी बड़ा हो सकता है, इसलिए streaming average इस्तेमाल करना बेहतर हैउदाहरण के लिए
new_mean = ((n*old_mean)+temp)/(n+1)जैसा तरीकादिलचस्प challenge है, लेकिन सिर्फ Java तक सीमित होना अफ़सोस की बात है। लोग जब खुद JVM bytecode हाथ से बनाना शुरू करेंगे, उसका इंतज़ार है
[0] https://github.com/gunnarmorling/1brc/discussions
मज़ेदार है। Advent of Code के बाद की अनौपचारिक मस्ती जैसा लगता है
अगर भाषाओं के बीच निष्पक्ष तुलना करनी है, तो make और build time भी शामिल होना चाहिए। Java/Maven को मैंने कुछ सालों से इस्तेमाल नहीं किया, लेकिन
./mvnw clean verifyका download 2 मिनट से चलता देख कर वजह फिर याद आ गईऔर incremental compile build tool के तौर पर Gradle ज़्यादा तेज़ है
programming सीखने में लगा समय का उचित अनुपात भी जोड़ना चाहिए
ऐसे challenge में बहुत naïve version के जीतने की संभावना काफ़ी ज़्यादा होगी, और यह न सिर्फ अवास्तविक है बल्कि challenge के उद्देश्य के भी ख़िलाफ़ है
cleanक्यों किया जा रहा हैcache फेंक कर फिर उसे slow कहना जैसी बात है
लिखा है कि external dependencies इस्तेमाल नहीं की जा सकतीं
Czech Technical University के C course में बिल्कुल ऐसा ही एक assignment था
सभी छात्रों की submissions का लगातार leaderboard पर मूल्यांकन होता था, और बेहतर grade के लिए extra points, यानी लगभग status points, पाने के लिए कई छात्र optimization पर दर्जनों घंटे खर्च करते थे
पहला स्थान 6 सेकंड है.. वाकई हैरान करने वाला है।