Ubuntu पैकेज को फिर से बिल्ड करके 90% अधिक तेज़ बनाना
(gist.github.com/jwbee)- उसी
jqसोर्स को दोबारा बिल्ड करके allocator बदलने पर, 500MB GeoJSON प्रोसेसिंग समय 4.606 सेकंड से 2.428 सेकंड तक घट गया, जिससे यह Ubuntu बाइनरी से 1.90 गुना तेज़ हो गया - बेंचमार्क Ryzen 9 9950X पर Alameda County Assessor parcel map के लिए
TotalNetValue < 193000शर्त केSitusCityनिकालने के तरीके से किया गया - सिर्फ साधारण rebuild से भी 2~4% सुधार मिला, और
clang-18,-O3,-flto,-DNDEBUGसंयोजन ने Ubuntu पैकेज की तुलना में 1.20 गुना प्रदर्शन दिया - प्रोफाइल में मेमोरी allocation लागत काफी बड़ी दिखी, इसलिए
TCMalloc,jemalloc,mimallocकी तुलना की गई;LD_PRELOADप्रयोग मेंmimallocसबसे तेज़ रहा - अंतिम
mimallocलिंक्ड बिल्ड ने अलग 2.2GB JSON प्रोसेसिंग मामले में भी 0.755 सेकंड बनाम 1.424 सेकंड दर्ज किया, जिससे पता चलता है कि workload के अनुसार distro के डिफ़ॉल्ट बिल्ड से बड़ा अंतर आ सकता है
आधारभूत workload और मापने का तरीका
- टेस्ट का लक्ष्य JSON प्रोसेसिंग टूल
jqहै, और इनपुट डेटा Alameda County Assessor के parcel map वाला 500MB GeoJSON फ़ाइल है - चलाया गया query parcel सूची में
TotalNetValue < 193000शर्त पूरी करने वाले आइटमों काSitusCityआउटपुट करता है.features[] | select(.properties.TotalNetValue < 193000) | .properties.SitusCity
- Ubuntu का डिफ़ॉल्ट
/usr/bin/jqफ़ाइल cache में होने पर लगभग 5 सेकंड लेता था, और विस्तृत बेंचमार्कhyperfineसे बार-बार मापा गया - रन के दौरान बदलाव कम करने के लिए
taskset -c 2से इसे logical CPU 2 पर pin किया गया- यह सेटिंग CPU 0 पर चलने वाले system interrupt और CPU migration के असर से बचने के लिए थी
उसी सोर्स का साधारण rebuild
- Ubuntu द्वारा उपयोग किए गए
jqsource code को लेकर बिना किसी अलग flag के configure और build किया गया - सिर्फ इस साधारण rebuild से भी Ubuntu बाइनरी पैकेज की तुलना में लगभग 2~4% अधिक गति मिली
- rebuilt बाइनरी: औसत 4.517 सेकंड
- Ubuntu
/usr/bin/jq: औसत 4.641 सेकंड - नतीजतन लगभग 1.03 गुना प्रदर्शन मिला
clang और optimization flags का उपयोग
- अगले चरण में
clang-18, ऊँचा optimization level, LTO, और debugging/profiling से जुड़े flags को साथ में लागू किया गया - प्रदर्शन पर असर डालने वाले मुख्य flags थे
-O3,-flto,-DNDEBUG-O3,-O2से ऊँचा optimization level उपयोग करता है-fltolink-time optimization को सक्षम करता है-DNDEBUGप्रोफाइल में बड़े दिखे assertion cost को कम करता है
- उपयोग किया गया configure उदाहरण इस प्रकार है
CC=clang-18LDFLAGS="-flto -g -Wl,--emit-relocs -Wl,-z,now -Wl,--gc-sections -fuse-ld=lld"CFLAGS="-flto -DNDEBUG -fno-omit-frame-pointer -gmlt -march=native -O3 -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections"
- यह बिल्ड Ubuntu बाइनरी की तुलना में 1.20 गुना तेज़ था
- optimized rebuild: औसत 3.853 सेकंड
- Ubuntu
/usr/bin/jq: औसत 4.631 सेकंड
allocator बदलने का प्रयोग
jqएक जटिल C प्रोग्राम है, और प्रोफाइल में मेमोरी allocation सबसे बड़ा खर्च दिखा- पहले Ubuntu पैकेज के रूप में उपलब्ध
TCMallocको लिंक करके फिर से build किया गयाLDFLAGSमें-L/usr/lib/x86_64-linux-gnu -ltcmalloc_minimalजोड़ा गया- rebuilt बाइनरी: औसत 3.253 सेकंड
- Ubuntu
/usr/bin/jq: औसत 4.611 सेकंड - यानी Ubuntu बाइनरी की तुलना में 1.42 गुना तेज़ परिणाम मिला
- डिफ़ॉल्ट Ubuntu बाइनरी में सिर्फ
LD_PRELOADसे allocator बदलने पर भी कुछ सुधार संभव था- डिफ़ॉल्ट: औसत 4.601 सेकंड
- TCMalloc preload: औसत 4.082 सेकंड
- डिफ़ॉल्ट की तुलना में 1.13 गुना तेज़
dynamic preload और THP सेटिंग
- Ubuntu द्वारा उपलब्ध
jemalloc,mimalloc,TCMallocकोLD_PRELOADके साथ तुलना की गई - यह तुलना नीचे दिए गए environment variables सेट करने के बाद मिले परिणामों पर आधारित है
MIMALLOC_LARGE_OS_PAGES=1MALLOC_CONF="thp:always,metadata_thp:always"GLIBC_TUNABLES=glibc.malloc.hugetlb=1
- नतीजतन mimalloc सबसे तेज़ निकला
- डिफ़ॉल्ट glibc: औसत 4.123 सेकंड
- TCMalloc preload: औसत 4.130 सेकंड
- jemalloc preload: औसत 3.510 सेकंड
- mimalloc preload: औसत 3.154 सेकंड
- THP सक्षम करने से glibc allocator, jemalloc, और mimalloc तीनों को फायदा मिला
THP + mimalloc,THP + glibcसे 31% तेज़ था, और glibc डिफ़ॉल्ट से 48% तेज़ था
mimalloc लिंक्ड बिल्ड का अंतिम परिणाम
- dynamic preload को प्रदर्शन के लिहाज़ से आदर्श नहीं माना गया, इसलिए अंतिम चरण में
mimallocको लिंक करकेjqको फिर से build किया गया - अंतिम बिल्ड Ubuntu बाइनरी पैकेज की तुलना में 1.90 गुना तेज़ था
mimallocrebuilt: औसत 2.428 सेकंड- Ubuntu
/usr/bin/jq: औसत 4.606 सेकंड - हर बेंचमार्क 10 रन के आधार पर था
- वही बिल्ड दूसरे application में भी उपयोग किया गया
- 2.2GB JSON को 13,000 फ़ाइलों में प्रोसेस किया गया
- parallelization के लिए
rushका उपयोग हुआ mimallocrebuiltjq: 0.755 सेकंड- Ubuntu पैकेज
jq: 1.424 सेकंड
- इस अलग मामले में भी गति सुधार लगभग 2 गुना के करीब था
1 टिप्पणियां
Hacker News की राय
“एक Ubuntu पैकेज को फिर से build करके और memory allocator बदलकर 90% तेज़ बनाना” जैसे clickbait पर तो TCP/IP के उस पार जाकर एक थप्पड़ मारने का मन करता है। यह सिर्फ़ एक पैकेज था, और कुछ सुधार recompile की वजह से भी नहीं थे
फिर भी मैंने jemalloc को
LD_PRELOADके ज़रिए एक program में लगाकरmallocimplementation बदलकर देखा था, और नतीजे काफ़ी अच्छे थे। performance मापी नहीं थी, लेकिन उस application की memory usage stable हो गई और memory leak जैसा दिखने वाला issue भी हल हो गया। असल में यह app की अपनी समस्या कम और standardmallocकी memory fragmentation होने की संभावना ज़्यादा थीfree()call करने पर भी, असाधारण स्थितियों को छोड़कर, बाहर से memory सच में release नहीं होतीthreads और CPU cores जितने ज़्यादा होते हैं, यह समस्या उतनी बढ़ती है। एक आसान समाधान “magic” environment variable
MALLOC_ARENA_MAX=2set करके caches की संख्या limit करना है। दूसरा तरीका यह है कि application नियमित रूप सेmalloc_trim()call करके cache खाली करे, लेकिन इसके लिए source में बदलाव चाहिएhttps://www.joyfulbikeshedding.com/blog/2019-03-14-what-caus...
उल्टा, संभावित रूप से bugs ला सकने वाले
-O3से compile नहीं करना अच्छी बात है। performance-critical कुछ हिस्सों के लिए यह अच्छा हो सकता है, लेकिन पूरे system को-O3से compile करना मैं नहीं चाहूंगाबहुत पहले मैंने Mozilla और अपने Linux kernel को अपनी पसंद के हिसाब से build करना शुरू किया था, और आम तौर पर ठीक-ठाक performance improvement मिलता था। Gentoo Linux distribution का पूरा मकसद भी, उदाहरण के लिए, हर चीज़ को source से optimized compile करके performance gain पाना ही है
jq,grep,ffmpeg,ocrmypdfजैसी कुछ common utilities में CPU bottleneck हो, और ऐसी general Unix utilities अक्सर किसी specific application के बजाय general-purpose build target के तौर पर बनाई जाती हैंEngineering समझौता है। लेख में ज़्यादातर फ़ायदा memory allocator को specialize करने से आया। यह याद रखना चाहिए कि कुछ projects multi-threaded होते हैं, एक thread में allocation करते हैं, दूसरे thread में data लिखते हैं और तीसरे thread में free भी कर देते हैं।
allocator को यह सब handle करना पड़ता है, इसलिए एक project की speed boost दूसरे project में crash बन सकती है। reallocation strategy भी समस्या है। कुछ programs पहले से allocate कर लेते हैं और फिर कभी
mallocको नहीं छूते, लेकिन दूसरे programs लगातार free करके फिर से लेते रहते हैं। fragmentation को कितनी अच्छी तरह handle किया जाता है, uptime 10 सेकंड है या 10 साल—यह भी मायने रखता है। कभी-कभी allocator का चुनाव long-term stability और short-term speed के बीच का फर्क बन जाता है।4K video test करते हुए frames cache करने वाला video editor बनाते समय मैंने कई allocators आज़माए थे। 32MB per frame, 60fps पर एक track के लिए लगभग 2GB प्रति सेकंड होता है। तुरंत allocator की limits से टकरा जाते हैं, और समझ आता है कि कम से कम default glibc allocator long-term stability के लिए सबसे अच्छा है। लेकिन छोटे benchmarks में वही सबसे धीमा होता है।
बेशक speed improvement की मात्रा अलग-अलग हो सकती है, लेकिन benchmarks आम तौर पर overall improvement दिखाते हैं। यह किसी specific application के लिए meaningful है या नहीं, यह बिल्कुल अलग मुद्दा है। crashes के बारे में भी, ये सभी general-purpose multi-threaded allocators हैं, इसलिए glibc से अलग व्यवहार नहीं करते, और bugs glibc में भी उतने ही हो सकते हैं।
DEFAULT_MMAP_THRESHOLD_MAXसे ऊपर चला जाता है, और 64-bit platforms पर यह 32MiB है, इसलिएmalloptmanual में documented तरीके से glibc को इसे cache करने के लिए राज़ी नहीं किया जा सकता।हर बार
mmapसे सीधे kernel से memory मांगी जाती है औरmunmapसे वापस की जाती है। ये system calls थोड़े slow हैं, और मेरे मामले में पहली access पर हर memory page पर page fault कराने की cost performance target पूरा न कर पाने जितनी धीमी है। solution सचमुच simple है। सिर्फ video frames के लिए general-purpose allocator याmmapके ऊपर अपनी free list इस्तेमाल कर लें। बिल्कुल same size की allocations बहुत reliably repeat होती हैं, इसलिए यह अच्छी तरह काम करता है।[1] UYVY format में 64MiB से थोड़ा कम, और I420 format में 48MiB से थोड़ा कम।
लेकिन यह parent comment पढ़कर चिंता हो रही है कि कहीं मैंने लेख को पूरी तरह गलत तो नहीं समझ लिया। tone से ऐसा लगता है जैसे gist में कही बात बिल्कुल नहीं करनी चाहिए, और यह ऐसी भयानक सलाह है जिसने इन सारी complexities को नज़रअंदाज़ कर दिया है। क्या आप समझने में मदद कर सकते हैं कि original gist अच्छा लेख है या नहीं, उसमें valid points हैं या नहीं, या उसकी कोई value ही नहीं है? यह comment देखने से पहले मुझे वह valuable लगा था, लेकिन अब एहसास हुआ कि शायद मैं फर्क समझने लायक smart नहीं हूँ।
data के हिसाब से सही compression algorithm इस्तेमाल करो, तो लोग बताते हैं कि तुम बेवकूफ क्यों हो और तुम्हें दूसरा algorithm इस्तेमाल करना चाहिए था। हाल ही में Dynamo में डालने के लिए एक specific लंबी JSON string compress करनी थी, और सभी popular algorithms को thoroughly test किया तो Brotli बहुत आगे निकला। फिर भी इससे हर राह चलते व्यक्ति को यह कहने से नहीं रोका जा सका कि zlib बेहतर है। कभी-कभी काफ़ी थका देने वाला होता है।
Gentoo Linux असल में ऐसे ही लोगों के लिए बनी distro है, ताकि वे अपनी Linux मशीन को अपने इस्तेमाल के हिसाब से optimize कर सकें
शुरुआती setup के बाद यह काफी simple और इस्तेमाल में आसान है। मुझे याद है कि Matrix के Gentoo Linux channel में मैंने कई दोस्त बनाए थे; मज़ेदार दिन थे
https://www.gentoo.org/
एक दिलचस्प बात: शुरुआती ChromeOS मूल रूप से custom Gentoo Linux install ही था। पता नहीं अब भी अंदरूनी तौर पर Gentoo Linux इस्तेमाल करता है या नहीं
मैंने Gentoo 20 साल इस्तेमाल किया है, लेकिन performance की वजह से कभी नहीं। जब आपको पता हो कि आप system से कैसा व्यवहार चाहते हैं, तो Gentoo शानदार है और वहाँ तक पहुँचने में मदद करता है
Gentoo का लक्ष्य ऐसा operating system रखना है जिसमें pre-built binary packages के बजाय सभी programs source से build होते हैं। इससे advanced speed improvements और customization संभव होता है, लेकिन इसका मतलब यह भी है कि kernel जैसे सबसे basic components भी source से compile करने पड़ते हैं। Linux community में यह अपने भारी-भरकम installation process की वजह से बहुत complex operating system माना जाता है। एक basic Gentoo installation सीधे command prompt में boot होता है, और user को खुद disk partition करने, “Stage 3 tarball” नाम का package download करके extract करने, packages manually install करने और system बनाने की जरूरत होती है। नए या कम अनुभवी users अक्सर installer में जाने पर graphical screen न दिखे तो समझ नहीं पाते कि क्या करें। /g/ members अक्सर Gentoo की value बढ़ा-चढ़ाकर बताते हैं ताकि नए users को install करने की कोशिश में फँसा सकें
एक-दो बार अटक सकता है, लेकिन अगर आपका overall Linux experience पर्याप्त है, तो build recipe में जाकर उसे ठीक करने, अपनी जरूरत के हिसाब से चलाने और fixes upstream contribute करने की संभावना ज्यादा है। यह minimalism पर जबरदस्त focus और किसी भी तरह की overengineering से बचने का परिणाम है। Gentoo इस्तेमाल करते हुए मुझे यही हिस्सा हमेशा खलता था। Gentoo में मैं हमेशा USE flags और package masks को ऐसे तरीकों से छेड़ता रहता था जो दूसरे users के लिए खास मददगार नहीं होते। Build system इतना complex था कि लंबे समय तक उसे सही से सीखना, root-cause level पर issues fix करना और upstream contribute करना बहुत मुश्किल था। अगर आप पूरा system source से build नहीं करना चाहते, लेकिन distro-provided binaries और खुद source से build किए packages को मिलाकर इस्तेमाल करना चाहते हैं, तो Void एक ideal base हो सकता है
इसके बाद ArchLinux पर चला गया, और मेरे लिए वह कुल मिलाकर ठीक रहा। अगर आप काफी standard processor इस्तेमाल कर रहे हैं, तो मुझे नहीं लगता Gentoo इतना बड़ा फायदा देगा
ऐसा करने पर आप सिर्फ
jqही नहीं, बल्कि regex parsing dependency onigurama के security updates से भी बाहर हो जाएंगे। पहले onigurama security update आया था, और अगर ऐसा फिर हुआ तो आप vulnerable हो सकते हैं।jqअक्सर untrusted JSON parse करने के लिए इस्तेमाल होता हैउसमें लिखा था, “security update: multiple invalid pointer dereferences, out-of-bounds write memory corruption, stack buffer overflow fixes”, और वह CVE-2017-9224, CVE-2017-9226, CVE-2017-9227, CVE-2017-9228, CVE-2017-9229 के लिए था
libonig5नाम के अलग package में है, और वह normal तरीके से update होगाCVE system की value कम करके नहीं आँकना चाहता, लेकिन findings के बीच real-world impact में बड़ा फर्क होता है, यह भी नकारना मुश्किल है
इस तरह की चीज़ों से निपटे हुए मुझे कुछ समय हो गया है, लेकिन मेरी याद में upstream developers जो flags इस्तेमाल करते हैं, उनसे आगे जाते ही अजीब bugs और bug आने पर जबरदस्त उदासीनता मिलती है। यहाँ मेरा मतलब distro packager से नहीं, upstream developer से है
libc के अलावा
mallocमैंने कभी इस्तेमाल नहीं किया, लेकिन वही principle लागू होगा लगता हैलेकिन अगर सब ऐसा करें तो वह monoculture बन जाता है, और monoculture fragile और खराब होता है। Code तभी थोड़ा मजबूत बनता है जब वह अलग contexts—यानी अलग platforms, compilers, options, libraries वगैरह—में build होता है। कोई bug जिसे ज्यादातर लोगों ने इसलिए नहीं छुआ क्योंकि उनका platform या build flags संयोग से trap के ठीक बगल से निकल गए, फिर भी bug ही है; उसे ढूँढना और fix करना code के लिए बेहतर है। Individuals के तौर पर हम सबको फायदा होता है जब code कुल मिलाकर fragile होने के बजाय robust बनता है
बेशक, मैं यह भी सोचता था कि
-march=nativeही मुझे दिखने वाला मुख्य improvement है, लेकिन यह article दिखाता है कि जरूरी नहीं ऐसा ही हो। Floating-point इस्तेमाल करने वाली applications में rough edges ज्यादा होने की संभावना भी लगती हैयह कुछ हद तक किसी खास workflow में benchmark अच्छे दिखाने वाले दूसरे allocator के साथ फिर से build करने जैसा है
mallocसे बेहतर तो लगभग कुछ भी हो सकता है। distros का mimalloc या jemalloc के बजाय लगातार glibcmallocइस्तेमाल करना असल में कर्तव्य में चूक जैसा हैmallocकिस तरह के workloads के लिए अच्छा है?उत्सुकता है कि performance की तुलना इस Rust-आधारित
jqclone से कैसी होगीcargo install --locked jaqकिसी खास CPU family के लिए optimization चालू करना हो तो
RUSTFLAGS="-C target-cpu=native"भी जोड़ सकते हैं।cargo installRust का एक underrated feature है, जो लेख में बताए गए तरह के use case के लिए बिल्कुल फिट बैठता है। यह tool को source से build करता है, इसलिए आप platform-specific features या instructions चुन सकते हैं जो पुराने CPUs के साथ compatibility के लिए आम तौर पर binaries में शामिल नहीं किए जाते। repository clone करने या build करने का तरीका पता लगाने की जरूरत भी नहीं होती, वह सब अपने आप साथ आ जाता हैjaq[1] औरyq[2] वे विकल्प हैं जिन्हें मैंjqइस्तेमाल करते समय जब भी तेज और आसान performance boost चाहिए होता है, चुनता हूँ[1] https://github.com/01mf02/jaq
[2] https://github.com/mikefarah/yq
jq,gojqसे करता हूँ, और AoC 2022 day 13 के लिए अपनेjqsolution से test करता हूँhttps://gist.github.com/oguz-ismail/8d0957dfeecc4f816ffee79d...
यह अभी भी दोनों से पीछे है
cargo installमें repository URL specify करने के लिए--gitflag भी है। इसका इस्तेमाल तब हो सकता है जब कोई public package न हो या आप latest commit चाहते हों जो अभी release नहीं हुआ हैमैंने पहले भी इसे कई बार इस्तेमाल किया है, खासकर अपने लिए जल्दबाजी में बनाए गए tool को repository में push करने के बाद उसे जल्दी install करने के आसान तरीके के रूप में—बिना release process बनाने, binaries को अपनी निजी machines पर manually copy करने, या build में इस्तेमाल हुए exact commit को track करने की जरूरत के
अगर वाकई ऐसा करना है, तो Ubuntu से वही source package download करवाना चाहिए जो वह चाहता है। इस case में
apt-get source jqइस्तेमाल करेंफिर package के अंदर जाकर मनचाहे तरीके से फिर से compile कर सकते हैं। distribution या archival के लिए उसे फिर से package भी कर सकते हैं। ऐसा करने से अजीब errors और mismatches का ढेर मिलने के बजाय, आपको upstream Ubuntu के काफी ज्यादा करीब result मिलेगा
title misleading है। मतलब तेज हुए time का 90% है, और असल में यह करीब 45% तेज है
अगर आपको इस बात में दिलचस्पी है कि हम भाषा का इस्तेमाल कैसे करते हैं, तो यह थोड़ा रोचक है। आप कह सकते हैं कि उसी समय में 90% ज्यादा काम होता है, और यह उन दूसरी speed units से मेल खाता है जिन्हें हम आम तौर पर इस्तेमाल करते हैं, जैसे miles per hour, words per minute, bits per second। लेकिन computer performance में convention यह है कि fixed amount of work में लगने वाले time को measure किया जाता है। शायद इसलिए कि आम तौर पर workload fixed होता है और बदलती है तो इंतजार की अवधि। इस blog post के मामले में भी ठीक यही है, इसलिए time को numerator में रखा जाता है। लेख खुद बहुत दिलचस्प और अच्छी तरह लिखा गया है, लेकिन यह 90% faster नहीं है
ऊपर से अगर time unit इस्तेमाल करनी हो, तो “faster” शब्द इस्तेमाल नहीं किया जाएगा। “45% less time” और “45% faster” बहुत अलग claims हैं, और दोनों programming के अंदर और बाहर मायने रखते हैं
जब हम कहते हैं “किसी चीज को N% घटाया”, तो आम तौर पर माना जाता है कि वह N% उसी चीज का है जिसे घटाया गया है, किसी दूसरे value का नहीं
यह पढ़कर कि इतने simple change से बड़ा speedup मिल सकता है, मेरे मन में सबसे पहले यही आया कि jq authors को बताना चाहिए। इसमें सावधान रहने लायक pitfalls हो सकते हैं, या test करने के बाद वे इसे सभी के लिए faster बना सकते हैं
नतीजा चाहे जो हो, बस inform कर देना useful लगता है। लेकिन लेख में यह option consider तक किया हुआ नहीं लगता, और यहाँ comments में भी दिखाई नहीं देता। क्या मैं कुछ miss कर रहा हूँ?
पता नहीं वहाँ भी glibc allocator standard है या नहीं
https://en.m.wikipedia.org/wiki/Clear_Linux_OS