- Apache OpenDAL की Python binding में file read, Python के built-in
open().read()से धीमा होने की रिपोर्ट से शुरुआत हुई, लेकिन bottleneck OpenDAL या PyO3 खुद नहीं था - 64MiB file read benchmark में
python-fs-readलगभग 15~19ms, जबकि Ruststd::fsऔर C implementation लगभग 23ms मापे गए, जिससे Rust/C, Python से धीमे लग रहे थे strace,eBPF,perfसे पीछा करने पर फर्कreadsyscall के destination buffer के page के अंदर स्थित offset से जुड़ा मिला, और0x10के आसपास performance drop दोबारा दिखाई दिया- AMD Ryzen 9 5900X, Ryzen 7 5700X, Ryzen 9 5900HX श्रृंखला में समान घटना की पुष्टि हुई, और kernel
_copy_to_iterके भीतरrep movsbexecution performance मुख्य सुराग था - Python मूल रूप से तेज़ नहीं था; यह AMD Zen 3 के FSRM/
rep movsbसे जुड़ा CPU bug और memory offset के संयोग का नतीजा था, औरjemallocसे दिखा सुधार भी allocator के कारण नहीं बल्कि अलग offset की वजह से था
OpenDAL Python binding से शुरू हुआ अजीब benchmark
- Apache OpenDAL कई storage services में data को unified तरीके से पढ़ने-लिखने के लिए एक data access layer है, और Python binding PyO3 के जरिए दी जाती है
- एक उपयोगकर्ता ने बताया कि OpenDAL Python binding से 150MB file पढ़ने वाला code, Python के built-in file read से धीमा है
- Python built-in
open(...).read()100 बार:4.470868484000675 - OpenDAL Python binding 100 बार:
8.993250704006641
- Python built-in
- सरल किए गए 64MiB file read में भी OpenDAL binding ज़्यादा धीमी थी
python-fs-read: औसत 15.9mspython-opendal-read: औसत 32.9ms- Python built-in read, OpenDAL binding से 2.07 गुना तेज़ मापा गया
Rust OpenDAL और std::fs तक पहुँची जाँच
- वही logic Rust के OpenDAL
fsservice में लागू करने पर भी यह Python built-in read से धीमा थाrust-opendal-fs-read: औसत 23.8mspython-fs-read: औसत 15.6ms- Python built-in read, Rust OpenDAL implementation से 1.52 गुना तेज़ मापा गया
- OpenDAL की
fsservice Rust std::fs का उपयोग करती है, इसलिए OpenDAL का खुद का overhead जाँचने के लिएstd::fsआधारित implementation अलग से लिखी गई - Rust
std::fsdirect implementation में भी वही पैटर्न जारी रहाrust-std-fs-read: औसत 23.1mspython-fs-read: औसत 15.2ms- Python built-in read, Rust
std::fsसे 1.52 गुना तेज़ मापा गया
strace में दिखे syscall और mmap
straceanalysis में Rust और Python दोनों ने बड़े buffer allocation के लिएmmapका उपयोग किया- Rust
std::fsexecution में/tmp/fileखोलना, 64MiB एक बार पढ़ना, EOF जाँच के लिएreadcall करना, फिर बंद करना—ऐसा flow था - Python built-in read ने
newfstatat,ioctl,lseekजैसी अधिक syscalls चलाईं, फिर भी कुल समय कम था mmap(NULL, 67112960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)call file mapping नहीं बल्कि anonymous memory allocation के लिए इस्तेमाल हुई थी67112960का मतलब 64MiB में 4KiB जोड़कर बना size हैMAP_ANONYMOUSका मतलब file से असंबंधित memory allocation है
- Rust का
x86_64-unknown-linux-gnudefault build,glibcकेmallocका उपयोग करता है, औरglibcबड़े allocations के लिएmmapका उपयोग कर सकता है
jemalloc से तेज़ हुआ Rust और पलटा हुआ बीच का निष्कर्ष
- Rust global allocator को
jemallocator::Jemallocमें बदलने पर यह Python से तेज़ हो गयाrust-std-fs-read-with-jemalloc: औसत 9.7mspython-fs-read: औसत 15.8msjemallocइस्तेमाल करने वाली Rust implementation, Python से 1.64 गुना तेज़ मापी गई
- इस बिंदु पर
mmapया default memory allocator कारण लग रहे थे, लेकिन बाद के update में यह व्याख्या सुधारी गई - 2023-12-01 update के अनुसार
jemalloc,pymalloc,mimallocकाglibc mallocसे मूल रूप से तेज़ होना इस फर्क का कारण नहीं था - असली फर्क allocator द्वारा बनाए गए buffer के page के भीतर offset से आया
rust-std-fs-read:mmapstart address से0x10offset पर readrust-std-fs-read-with-jemalloc:mmapstart address से0x740offset पर read
- समस्या वाला दायरा page के भीतर
0x00..0x10range के रूप में सामने आया, औरjemallocमें भी वही समस्या दोबारा दिखाई जा सकती है
software settings से ज़्यादा device-specific reproducibility वाली समस्या
- चर्चा आगे बढ़ने पर यह पुष्टि हुई कि Rust का Python से धीमा दिखना खास तौर पर लेखक की machine पर बहुत स्पष्ट था
- लेखक का CPU AMD Ryzen 9 5950X 16-Core Processor था, और memory DDR4 3200 MT/s 16GB DIMM configuration में थी
- कई settings बदलने पर भी relative performance gap गायब नहीं हुआ
- Linux kernel
mitigations=offफिर से चालू करने पर भी नतीजों में बदलाव नहीं आया - Transparent Hugepage को
always,madvise,neverमें बदलने पर absolute values बदलीं, लेकिन relative ratio बना रहा core_affinityसे किसी खास CPU core पर pin करने पर भी नतीजे वही रहे
- Linux kernel
- eBPF-आधारित
readsyscall latency measurement में भी Rust वाला path ज़्यादा धीमा था- Python
read file: 8,134,049ns - Rust
std::fsread file: 24,636,975ns
- Python
- अवलोकनों से यह साफ हुआ कि OpenDAL, PyO3, Rust standard library भर से इस फर्क को समझाना मुश्किल था; syscall स्तर पर ही समय का अंतर बन चुका था
C implementation में सामने आया memory offset का सुराग
- वही 64MiB file read अगर C
fopen/malloc/freadसे लागू की जाए, तब भी यह Python से धीमी थीc-fs-read: औसत 23.8mspython-fs-read: औसत 19.1ms- Python built-in read, C implementation से 1.25 गुना तेज़ मापी गई
strace -e raw=read,mmapसे pointer addresses देखने पर C और Python के buffer start offset अलग थे- C:
mmapreturn address से0x10offset परread - Python:
mmapreturn address से0x30offset परread
- C:
- C implementation में उसी तरीके से offset adjust करने पर performance बहुत सुधर गई
c-fs-read-with-offset: औसत 8.9ms- Python से 2.15 गुना, और पुरानी C implementation से 2.68 गुना तेज़
- यह समस्या AMD Ryzen 9 5900X और AMD Ryzen 7 5700X पर भी दोबारा देखी गई
- Rust community की Std::fs::read slow? चर्चा में भी ऐसा ही behavior रिपोर्ट हुआ, और memory region offset तथा syscall performance के बीच संबंध की ओर इशारा किया गया
perf analysis ने rep movsb की ओर इशारा किया
- एक kernel developer ने
AMD Ryzen 9 5900HXपरc-fs-readऔर offset लागू किए गए version को reproduce करकेperfसे analysis किया - offset होने या न होने के आधार पर
L1-dcache-prefetchesऔरL1-dcache-loadsvalues में बड़ा अंतर था- offset नहीं:
L1-dcache-loadsलगभग 127,845,213,L1-dcache-prefetchesलगभग 1,843,493 - offset के साथ:
L1-dcache-loadsलगभग 13,965,813,L1-dcache-prefetchesलगभग 395,578
- offset नहीं:
- hotspot, kernel के
readpath मेंshmem_file_read_iter→copy_page_to_iter→_copy_to_iterतक जाता था _copy_to_iterके भीतर मुख्य assemblyrep movsbथी, और ज़्यादातर samples इसी instruction पर केंद्रित थे- बाद की analysis में यह बात ज़्यादा महत्वपूर्ण निकली कि L1 prefetch खुद मुख्य कारण नहीं था, बल्कि page-aligned data पर
rep movsbperformance खराब थी और page alignment टूटने पर यह बेहतर हो जाती थी
FSRM और AMD Zen 3 की समस्या
- साझा की गई Ubuntu glibc bug report Terrible memcpy performance on Zen 3 when using rep movsb भी
rep movsbperformance problem पर चर्चा करती है - उस report के उदाहरण में 2113-byte copy पर
rep movsbpath लगभग 3.2GB/s दिखाता है, लेकिन size को 2111 bytes करने पर यह 100GB/s से ऊपर पहुँच जाता है - FSRM का मतलब Fast Short REP MOV है, जो
rep movsbऔरrep movsdको तेज़ बनाने के लिए एक feature है - FSRM, Intel से शुरू हुआ feature है और AMD में भी लाया गया; support घोषित करने वाले CPUs पर
glibcdefault रूप से FSRM का उपयोग करता है - इसलिए Python, C/Rust से मूल रूप से तेज़ नहीं था; बल्कि AMD CPU bug की वजह से कुछ खास memory offsets पर C/Rust का read path धीमा हो रहा था
अपडेट: AMD की जानकारी और glibc की प्रतिक्रिया
- 2023-12-01 update के अनुसार AMD को इस bug की जानकारी 2021 से थी
- लेख सार्वजनिक होने के बाद कई पाठकों ने AMD को यह link भेजा, इसलिए माना गया कि AMD इस समस्या से अवगत है
- लेखक का मानना है कि AMD को
amd-ucodeमें इस bug की जिम्मेदारी लेकर इसे ठीक करना चाहिए, लेकिन अपुष्ट जानकारी के अनुसार Zen 3 परamd-ucodefix मुश्किल हो सकता है - व्यावहारिक उम्मीद यह है कि
glibcजरूरत पड़ने पर FSRM को disable करे glibcकी ओर x86: Improve ERMS usage on Zen3 पर काम चल रहा है
reproduction code और संबंधित सामग्री
- Xuanwo/when-i-find-rust-is-slow: इस्तेमाल किए गए code snippets और scripts का संग्रह
- Std::fs::read slow?: Rust community की समान रिपोर्ट
- Terrible memcpy performance on Zen 3 when using rep movsb: Ubuntu glibc में रिपोर्ट की गई Zen 3
rep movsbperformance समस्या - binding/python: rust std fs is slower than python fs: OpenDAL Python binding से जुड़ा issue
1 टिप्पणियां
Hacker News की राय
दो अलग-अलग CPU feature flags हैं जो बताते हैं कि
REP STOS/MOVतेज़ है और इसेmemset/memcpyके छोटे instruction sequences के तौर पर इस्तेमाल किया जा सकता हैहर नई CPU generation के साथ optimization routines को हाथ से फिर लिखने की दशकों पुरानी तकलीफ अभी भी जारी है—ऐसा लगता है कि यह CPU vendors के timing test suite में होना चाहिए
संभव है कि page-aligned तेज़
rep movsमें कोई समस्या रही हो, या वह किसी attack के लिए vulnerable रहा हो इसलिए disable कर दिया गया होfix किस तरह का होना चाहिए, क्या runtime check जैसी चीज़ चाहिए—यह समझ नहीं आ रहा
अगर कोई तेज़ “software” implementation है, तो सवाल है कि
REP MOVSको कम-से-कम microcode में वही काम करवाने जैसा क्यों नहीं बनाया जातासंबंधित glibc bug यहां है। हालांकि यह Zen 4 वाला है: https://sourceware.org/bugzilla/show_bug.cgi?id=30994
शुरुआत में लेख पढ़कर मैं लेखक पर हंसने के लिए तैयार था कि उसने
std::fsगलत इस्तेमाल किया होगा, लेकिन असल में यह debugging rabbit hole और mystery से भरा एक मजेदार लेख निकलाअच्छा लिखा था और बहुत दिलचस्प था
premise थोड़ा confusing है। तुलना pure Python code और native C/Rust code की नहीं थी, बल्कि native code के ऊपर Python wrapper यानी Python file read method और दूसरे native code wrapper OpenDAL की थी
performance difference होना फिर भी दिलचस्प है, लेकिन इसे “Python से धीमा” कहना काफी अजीब है। क्या उम्मीद थी कि Python standard library पूरी तरह pure Python में लिखी है? बल्कि मैं तो उम्मीद करूंगा कि Python standard library के function implementations native होंगे और अलग-अलग स्तर पर काफी optimized होंगे
निष्कर्ष native code के behavior से जुड़ा निकला, यह हैरानी की बात नहीं थी; लेकिन असली जवाब अप्रत्याशित था। बस शुरुआत confusing थी, लेख खुद बहुत दिलचस्प था
और “C is slower than Python with specified offset” शीर्षक भी native speaker को “offset specify करने के बाद भी C, Python से धीमा है” जैसा पढ़ता है। असल में मतलब उल्टा था—Python में जो offset इस्तेमाल हो रहा था, उसे C में भी specify करने पर C तेज़ हो गया
file read जैसा सरल काम Rust standard library में Python standard library से धीमा होना चौंकाने वाली बात है। भले ही पता हो कि ऐसे Python standard library calls C में लिखे हैं, फिर भी उम्मीद होगी कि Rust standard library call भी लगभग उतना ही तेज़ होगा
इसलिए सामान्य तौर पर लगेगा कि usage गलत है या Rust standard library में कोई अजीब behavior है, लेकिन इस बार दोनों में से कुछ नहीं था; यह खास hardware पर allocation alignment के कारण आने वाली performance cliff थी
file system read Python में अच्छी तरह optimized होगा, ऐसी उम्मीद है, लेकिन Rust में भी वैसी ही उम्मीद होगी—इसलिए Rust वाला काफी धीमा निकला, यह चौंकाने वाला था, और खासकर यह कि यह hardware और allocator पर depend करता था, और भी ज्यादा चौंकाने वाला
Python में लिखा code अगर तेज़ है, तो मेरे लिए Python तेज़ है। implementation किसी दूसरी भाषा में है या वजह कुछ और है, यह बहुत मायने नहीं रखता
original post में जो हुआ वह लगभग पूरी तरह संयोग था। CPython का C code
constconsistency तक की परवाह नहीं करता, और dynamic memory allocation तथा helper/convenience calls बहुत होते हैं। arithmetic जैसी चीज़ें भी dynamic memory allocation करती हैंअगर आपने CPython के साथ काम किया है, तो आम तौर पर अच्छी performance की उम्मीद नहीं करेंगे। performance सुधारनी हो तो आप वहां दी गई functionality को bypass करने की कोशिश करते हैं
इसके अलावा Python का कोई standard नहीं है, इसलिए सख्ती से कहें तो standard library भी नहीं है, और साथ में distribute होने वाली library ज्यादातर Python में लिखी है। कुछ हिस्से C में लिखे हैं, लेकिन उस C code में भी काफी हिस्सा असल में Python code को mechanically C में port करने जैसा है। उदाहरण के लिए Python का binary search implementation मूल रूप से Python में लिखा गया था, फिर बाद में Python C API का इस्तेमाल करके C में translate किया गया
ज्यादा-से-ज्यादा यह उम्मीद की जा सकती है कि operating system functions पर सीधे map होने वाली functionality के लिए wrapper अपेक्षाकृत thin होगा। यानी file read मूल रूप से सीधे system interface में जाता है, इसलिए binding code ज्यादा नहीं चाहिए होगा
ऐसी दर्जनों posts आने के बाद सबने यह समझ लिया
लेख अपने-आप में शानदार है और इस issue से जुड़ी काफी दिलचस्प जानकारी देता है
लेकिन जिस बात में ज्यादा दिलचस्पी और चिंता है, वह यह है कि issue कैसे report·record किया जाता है, और communication कैसे handle होती है
report Discord पर होती है, जो proprietary environment है, indexed नहीं है, search करना भी मुश्किल है और preserve भी नहीं होता। चर्चा Discord और Telegram पर होती है, और इस context में Telegram शायद और भी खराब हो सकता है
यह blog post और GitHub repository ही इसके निशान के तौर पर बची सारी चीजें हैं। अगर Xuanwo ने blog पर नहीं लिखा होता, तो यह timeline में गायब हो गया होता। काफी दिलचस्प स्थिति है
ऐसे messenger बहुत कम हैं जो default रूप से publicly accessible logs को index और search कराते हों। हर IRC server public logs provide नहीं करता, और Matrix groups भी ऐसे ही हैं। मुझे नहीं समझ आता कि वहां की discussions timeline में गायब क्यों नहीं मानी जातीं
public logs provide किए जा सकने की वजह यह नहीं है कि वे proprietary नहीं हैं, बल्कि यह है कि उनके पास logging allow करने वाली API है। Telegram में भी ऐसी API है, और हमारे discussion group के searchable logs यहां देखे जा सकते हैं: https://luoxu-web.vercel.app/#g=1264662201
public indexing न होने की वजह मुख्यतः privacy है, न कि platform का proprietary होना
पहले सभी posts को DejaNews, और बाद में Google पर साफ-सुथरे तरीके से search किया जा सकता था
internet/WWW stack और core programming tools·libraries जैसे महत्वपूर्ण open source projects की अहम communication open standards पर लौटनी चाहिए
इस हफ्ते पढ़े लेखों में यह सबसे दिलचस्प था। बेहतरीन summary है
स्पष्ट तौर पर करने वाला काम
copy_user_generickernel method के लिए patch भेजना लगता हैजब problematic CPU detect हो और वह memory alignment slow करने वाला bug trigger करे, तो अलग memory copy implementation इस्तेमाल करवाना चाहिए
kernel experience न रखने वाले व्यक्ति से accept हो सकने वाला fix मामूली नहीं होगा। इससे भी महत्वपूर्ण बात यह है कि workaround को किस तरीके से enable किया जाए, यह भी साफ नहीं है। शायद boot time पर measure करना सबसे अच्छा होगा, वरना यह कैसे पता चलेगा कि कौन-से models और steppings affected हैं, यह अस्पष्ट है
software mitigation भी जटिल होगी। kernel जब ERMS use नहीं कर सकता, तो आम तौर पर alternative path में इस्तेमाल होने वाले vector instructions को वास्तव में use नहीं कर सकता
jemalloc2018 तक Rust का default allocator थाhttps://internals.rust-lang.org/t/jemalloc-was-just-removed-...
“Rust डेवलपर performance बेहतर करने के लिए
jemallocatorपर स्विच करने पर विचार कर सकते हैं” वाला हिस्सा दिलचस्प हैसमझ नहीं आ रहा कि क्या कोई भी लगभग मुफ्त में performance boost पा सकता है, या इसमें कुछ सावधानियां हैं। यह भी जानना है कि क्या C codebase को भी फायदा हो सकता है, और क्या यह ऐसी performance है जिसे हम अभी बस miss कर रहे हैं
jemallocइस्तेमाल करने परMADV_FREEकी वजह से observability की समस्या आती है, यह जानना जरूरी है।htopअब वास्तव में इस्तेमाल हो रही memory को सही-सही नहीं दिखाताhttps://github.com/jemalloc/jemalloc/issues/387#issuecomment...
https://gitlab.haskell.org/ghc/ghc/-/issues/17411
लगता है अब
jemalloc,MADV_FREEके 10 सेकंड बादMADV_DONTNEEDcall करता है: https://github.com/JuliaLang/julia/issues/51086#issuecomment...इसलिए यह इस issue को “ठीक” तो करता है, लेकिन memory free करने के समय और
htopमें उस बात को observe करने के समय के बीच एक उलझाने वाली delay आ जाती हैहालांकि https://jemalloc.net/jemalloc.3.html के अनुसार
opt.muzzy_decay_ms = 0सेट करके delay हटाई जा सकती हैफिर भी musl के author
jemallocको default बनाने को लेकर सतर्क हैं: https://www.openwall.com/lists/musl/2018/04/23/2सार यह है कि इसमें गंभीर bloat, ASLR कमजोर होना, और memory usage की परवाह किए बिना जितना हो सके उतना fast बनाने पर केंद्रित optimizations जैसी समस्याएं हैं। ऊपर दिए tuning value से कुछ हद तक कमी लाई जा सकती है, लेकिन performance और memory usage में किस पर focus करना है—यह समग्र रुझान अब भी एक trade-off बने रहने की संभावना है
यह हर स्थिति में जरूरी नहीं कि faster हो, लेकिन ज्यादातर मामलों में faster होगा। Rust भी पहले
jemallocको default के रूप में इस्तेमाल करता था, लेकिन कुछ लोगों को यह default के तौर पर unexpected लगा, इसलिए इसे बदल दिया गयायह workload पर बहुत निर्भर करता है, इसलिए profiling और benchmarking जरूरी है। फिर भी C/C++/Rust जैसी low-level languages में ऐसे allocators चुनने की क्षमता होनी चाहिए
एक सावधानी binary size है। custom allocator executable में extra bytes जोड़ता है
jemallocको default के रूप में इस्तेमाल करता था, लेकिन 2018 के आसपास वापस systemmallocपर लौट गया[0]अब Rust में
GlobalAlloctrait और#[global_allocator]attribute हैं, इसलिए अगर app चाहे तोjemallocको allocator के रूप में इस्तेमाल कर सकती है। userLD_PRELOADजैसे तरीके से इसे override कर सकता है या नहीं, मुझे ठीक से नहीं पताjemallocहर workload और use case के लिए हमेशा best नहीं है। system allocator अक्सर perfect से काफी दूर होता है, लेकिन कम से कम उसे general-purpose allocator के रूप में व्यापक रूप से test किया गया है[0] https://github.com/rust-lang/rust/issues/36963
jemallocकुछ applications के लिए सही choice हो सकता है, लेकिन दूसरी cases में कोई और allocator ज्यादा fast हो सकता है। या फिर वह धीमा होते हुए भी कम dirty memory, बेहतर observability, या कुछ खास security guarantees जैसे goals के लिए बेहतर fit हो सकता हैयह content सही लोगों को भेज दिया