2 पॉइंट द्वारा GN⁺ 2023-11-30 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Apache OpenDAL की Python binding में file read, Python के built-in open().read() से धीमा होने की रिपोर्ट से शुरुआत हुई, लेकिन bottleneck OpenDAL या PyO3 खुद नहीं था
  • 64MiB file read benchmark में python-fs-read लगभग 15~19ms, जबकि Rust std::fs और C implementation लगभग 23ms मापे गए, जिससे Rust/C, Python से धीमे लग रहे थे
  • strace, eBPF, perf से पीछा करने पर फर्क read syscall के destination buffer के page के अंदर स्थित offset से जुड़ा मिला, और 0x10 के आसपास performance drop दोबारा दिखाई दिया
  • AMD Ryzen 9 5900X, Ryzen 7 5700X, Ryzen 9 5900HX श्रृंखला में समान घटना की पुष्टि हुई, और kernel _copy_to_iter के भीतर rep movsb execution 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
  • सरल किए गए 64MiB file read में भी OpenDAL binding ज़्यादा धीमी थी
    • python-fs-read: औसत 15.9ms
    • python-opendal-read: औसत 32.9ms
    • Python built-in read, OpenDAL binding से 2.07 गुना तेज़ मापा गया

Rust OpenDAL और std::fs तक पहुँची जाँच

  • वही logic Rust के OpenDAL fs service में लागू करने पर भी यह Python built-in read से धीमा था
    • rust-opendal-fs-read: औसत 23.8ms
    • python-fs-read: औसत 15.6ms
    • Python built-in read, Rust OpenDAL implementation से 1.52 गुना तेज़ मापा गया
  • OpenDAL की fs service Rust std::fs का उपयोग करती है, इसलिए OpenDAL का खुद का overhead जाँचने के लिए std::fs आधारित implementation अलग से लिखी गई
  • Rust std::fs direct implementation में भी वही पैटर्न जारी रहा
    • rust-std-fs-read: औसत 23.1ms
    • python-fs-read: औसत 15.2ms
    • Python built-in read, Rust std::fs से 1.52 गुना तेज़ मापा गया

strace में दिखे syscall और mmap

  • strace analysis में Rust और Python दोनों ने बड़े buffer allocation के लिए mmap का उपयोग किया
  • Rust std::fs execution में /tmp/file खोलना, 64MiB एक बार पढ़ना, EOF जाँच के लिए read call करना, फिर बंद करना—ऐसा 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-gnu default build, glibc के malloc का उपयोग करता है, और glibc बड़े allocations के लिए mmap का उपयोग कर सकता है

jemalloc से तेज़ हुआ Rust और पलटा हुआ बीच का निष्कर्ष

  • Rust global allocator को jemallocator::Jemalloc में बदलने पर यह Python से तेज़ हो गया
    • rust-std-fs-read-with-jemalloc: औसत 9.7ms
    • python-fs-read: औसत 15.8ms
    • jemalloc इस्तेमाल करने वाली 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: mmap start address से 0x10 offset पर read
    • rust-std-fs-read-with-jemalloc: mmap start address से 0x740 offset पर read
  • समस्या वाला दायरा page के भीतर 0x00..0x10 range के रूप में सामने आया, और 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 करने पर भी नतीजे वही रहे
  • eBPF-आधारित read syscall latency measurement में भी Rust वाला path ज़्यादा धीमा था
    • Python read file: 8,134,049ns
    • Rust std::fs read file: 24,636,975ns
  • अवलोकनों से यह साफ हुआ कि OpenDAL, PyO3, Rust standard library भर से इस फर्क को समझाना मुश्किल था; syscall स्तर पर ही समय का अंतर बन चुका था

C implementation में सामने आया memory offset का सुराग

  • वही 64MiB file read अगर C fopen/malloc/fread से लागू की जाए, तब भी यह Python से धीमी थी
    • c-fs-read: औसत 23.8ms
    • python-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: mmap return address से 0x10 offset पर read
    • Python: mmap return address से 0x30 offset पर read
  • 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-loads values में बड़ा अंतर था
    • 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
  • hotspot, kernel के read path में shmem_file_read_itercopy_page_to_iter_copy_to_iter तक जाता था
  • _copy_to_iter के भीतर मुख्य assembly rep movsb थी, और ज़्यादातर samples इसी instruction पर केंद्रित थे
  • बाद की analysis में यह बात ज़्यादा महत्वपूर्ण निकली कि L1 prefetch खुद मुख्य कारण नहीं था, बल्कि page-aligned data पर rep movsb performance खराब थी और page alignment टूटने पर यह बेहतर हो जाती थी

FSRM और AMD Zen 3 की समस्या

  • साझा की गई Ubuntu glibc bug report Terrible memcpy performance on Zen 3 when using rep movsb भी rep movsb performance problem पर चर्चा करती है
  • उस report के उदाहरण में 2113-byte copy पर rep movsb path लगभग 3.2GB/s दिखाता है, लेकिन size को 2111 bytes करने पर यह 100GB/s से ऊपर पहुँच जाता है
  • FSRM का मतलब Fast Short REP MOV है, जो rep movsb और rep movsd को तेज़ बनाने के लिए एक feature है
  • FSRM, Intel से शुरू हुआ feature है और AMD में भी लाया गया; support घोषित करने वाले CPUs पर glibc default रूप से 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-ucode fix मुश्किल हो सकता है
  • व्यावहारिक उम्मीद यह है कि glibc जरूरत पड़ने पर FSRM को disable करे
  • glibc की ओर x86: Improve ERMS usage on Zen3 पर काम चल रहा है

reproduction code और संबंधित सामग्री

1 टिप्पणियां

 
GN⁺ 2023-11-30
Hacker News की राय
  • दो अलग-अलग CPU feature flags हैं जो बताते हैं कि REP STOS/MOV तेज़ है और इसे memset/memcpy के छोटे instruction sequences के तौर पर इस्तेमाल किया जा सकता है
    हर नई CPU generation के साथ optimization routines को हाथ से फिर लिखने की दशकों पुरानी तकलीफ अभी भी जारी है—ऐसा लगता है कि यह CPU vendors के timing test suite में होना चाहिए

    • पूरी तरह अनुमान है, लेकिन यह आखिरी क्षण में या release के बाद microcode update के जरिए डाले गए bug fix का असर भी हो सकता है
      संभव है कि page-aligned तेज़ rep movs में कोई समस्या रही हो, या वह किसी attack के लिए vulnerable रहा हो इसलिए disable कर दिया गया हो
    • अगर मैंने सही समझा है, तो क्या इसका मतलब है कि किसी खास compile-time build के लिए दो executables बनाने होंगे, या फिर उसी खास hardware पर compile करना होगा?
      fix किस तरह का होना चाहिए, क्या runtime check जैसी चीज़ चाहिए—यह समझ नहीं आ रहा
    • यह सोचना आसान है कि CPU vendor अपने CPU को सबसे अच्छी तरह जानता होगा
      अगर कोई तेज़ “software” implementation है, तो सवाल है कि REP MOVS को कम-से-कम microcode में वही काम करवाने जैसा क्यों नहीं बनाया जाता
  • संबंधित glibc bug यहां है। हालांकि यह Zen 4 वाला है: https://sourceware.org/bugzilla/show_bug.cgi?id=30994

  • शुरुआत में लेख पढ़कर मैं लेखक पर हंसने के लिए तैयार था कि उसने std::fs गलत इस्तेमाल किया होगा, लेकिन असल में यह debugging rabbit hole और mystery से भरा एक मजेदार लेख निकला
    अच्छा लिखा था और बहुत दिलचस्प था

    • सच में बहुत अच्छा लेख था। test program बनाकर layers को एक-एक करके हटाने का debugging approach समझदार था, निष्कर्ष दिलचस्प और अप्रत्याशित था, और लेख इतना साफ था कि follow करना आसान रहा
  • 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 तेज़ हो गया

    • मुझे तो उल्टा यह समझ नहीं आता कि इसमें confusion क्यों है
      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 को धीमा होने पर धीमी भाषा कहकर कोसा जाता है, और तेज़ होने पर “यह असली Python नहीं है” कहकर credit नहीं दिया जाता
      Python में लिखा code अगर तेज़ है, तो मेरे लिए Python तेज़ है। implementation किसी दूसरी भाषा में है या वजह कुछ और है, यह बहुत मायने नहीं रखता
    • “अलग-अलग स्तर पर काफी optimized” होने की उम्मीद करने की वजह क्या है, यह समझ नहीं आता
      original post में जो हुआ वह लगभग पूरी तरह संयोग था। CPython का C code const consistency तक की परवाह नहीं करता, और 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 ज्यादा नहीं चाहिए होगा
    • बताने के लिए धन्यवाद। title बदल दिया
    • premise यह है कि “Python, Rust से तेज़ है” जैसी phrase लिखने पर, भले वह सच न हो, pageviews मिलते हैं
      ऐसी दर्जनों 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 में गायब हो गया होता। काफी दिलचस्प स्थिति है

    • यह proprietary platform है, यह सही है और अच्छी बात नहीं है। लेकिन indexing या search न होने का आरोप समझना मुश्किल है
      ऐसे 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 होना
    • USENET के पतन पर अफसोस जताते समय “अब Discord तो है” वाला जवाब मैं इसी वजह से स्वीकार नहीं करता
      पहले सभी posts को DejaNews, और बाद में Google पर साफ-सुथरे तरीके से search किया जा सकता था
      internet/WWW stack और core programming tools·libraries जैसे महत्वपूर्ण open source projects की अहम communication open standards पर लौटनी चाहिए
  • इस हफ्ते पढ़े लेखों में यह सबसे दिलचस्प था। बेहतरीन summary है

  • स्पष्ट तौर पर करने वाला काम copy_user_generic kernel method के लिए patch भेजना लगता है
    जब problematic CPU detect हो और वह memory alignment slow करने वाला bug trigger करे, तो अलग memory copy implementation इस्तेमाल करवाना चाहिए

    • यह इतना स्पष्ट नहीं है। अगर इसे microcode से fix किया जा सकता है, तो kernel में effectively software-patchable problem के fix code को जगह-जगह फैलाने के बजाय लोगों से updated microcode इस्तेमाल करवाना बेहतर लगता है
      kernel experience न रखने वाले व्यक्ति से accept हो सकने वाला fix मामूली नहीं होगा। इससे भी महत्वपूर्ण बात यह है कि workaround को किस तरीके से enable किया जाए, यह भी साफ नहीं है। शायद boot time पर measure करना सबसे अच्छा होगा, वरना यह कैसे पता चलेगा कि कौन-से models और steppings affected हैं, यह अस्पष्ट है
    • यह मामूली fix नहीं है। AMD को यह पता लगाना होगा कि page alignment के करीब addresses पर aliasing क्यों टूट रही है, इसलिए fix संभवतः microcode side पर होगा
      software mitigation भी जटिल होगी। kernel जब ERMS use नहीं कर सकता, तो आम तौर पर alternative path में इस्तेमाल होने वाले vector instructions को वास्तव में use नहीं कर सकता
  • jemalloc 2018 तक 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_DONTNEED call करता है: 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 बने रहने की संभावना है
    • मुझे यह लगभग मुफ्त में miss की जा रही performance लगती है। बस binary size थोड़ा बढ़ने की cost है
      यह हर स्थिति में जरूरी नहीं कि faster हो, लेकिन ज्यादातर मामलों में faster होगा। Rust भी पहले jemalloc को default के रूप में इस्तेमाल करता था, लेकिन कुछ लोगों को यह default के तौर पर unexpected लगा, इसलिए इसे बदल दिया गया
    • non-default allocator पर स्विच करने से हमेशा performance नहीं बढ़ती
      यह workload पर बहुत निर्भर करता है, इसलिए profiling और benchmarking जरूरी है। फिर भी C/C++/Rust जैसी low-level languages में ऐसे allocators चुनने की क्षमता होनी चाहिए
      एक सावधानी binary size है। custom allocator executable में extra bytes जोड़ता है
    • Rust पहले jemalloc को default के रूप में इस्तेमाल करता था, लेकिन 2018 के आसपास वापस system malloc पर लौट गया[0]
      अब Rust में GlobalAlloc trait और #[global_allocator] attribute हैं, इसलिए अगर app चाहे तो jemalloc को allocator के रूप में इस्तेमाल कर सकती है। user LD_PRELOAD जैसे तरीके से इसे override कर सकता है या नहीं, मुझे ठीक से नहीं पता
      jemalloc हर workload और use case के लिए हमेशा best नहीं है। system allocator अक्सर perfect से काफी दूर होता है, लेकिन कम से कम उसे general-purpose allocator के रूप में व्यापक रूप से test किया गया है
      [0] https://github.com/rust-lang/rust/issues/36963
    • performance कोई one-dimensional scale नहीं है जिस पर program “slow” से “fast” की ओर बढ़ता है। हमेशा दूसरे factors भी साथ काम करते हैं
      jemalloc कुछ applications के लिए सही choice हो सकता है, लेकिन दूसरी cases में कोई और allocator ज्यादा fast हो सकता है। या फिर वह धीमा होते हुए भी कम dirty memory, बेहतर observability, या कुछ खास security guarantees जैसे goals के लिए बेहतर fit हो सकता है
  • यह content सही लोगों को भेज दिया

    • क्या मतलब AMD वालों को भेजा?