1 पॉइंट द्वारा GN⁺ 2024-08-27 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • write से Linux पाइप में डेटा लिखना साधारण memory write की तुलना में काफी धीमा हो गया; प्रयोग वाले environment में यह memory buffer write 167GB/s के मुकाबले pipe write 17GB/s के स्तर पर ही रहा
  • bottleneck को केवल एक data copy से समझाया नहीं जा सकता; pipe_write के अंदर page allocation, locks और kernel copy routine की लागत मिलकर जमा होती है
  • vmsplice user-space buffer को kernel में copy किए बिना pipe से जोड़ देता है, जिससे __alloc_pages, _raw_spin_lock_irq, copy_user_enhanced_fast_string जैसे महंगे paths bypass होते हैं
  • Fizz Buzz throughput के मामले में vmsplice इस्तेमाल करने वाला solution single core पर 60.8GiB/s, multi-core पर 208.3GiB/s तक पहुँचा, और एक अलग प्रयोग के vmsplice example ने 210GB/s दर्ज किया
  • corrections की वजह से SIMD न इस्तेमाल करने की penalty वाली व्याख्या भरोसेमंद नहीं है, और inter-process communication सिर्फ L1 cache में पूरा नहीं होता, इसलिए 167GB/s को वास्तविक pipe throughput मानना मुश्किल है

शुरुआती बिंदु: vmsplice से बनने वाला बड़ा अंतर

  • कुछ programs pipe के जरिए data को ज्यादा तेजी से move करने के लिए vmsplice system call का इस्तेमाल करते हैं
  • Code Golf StackExchange की Fizz Buzz throughput competition में solutions मोटे तौर पर दो groups में बंटे
    • vmsplice न इस्तेमाल करने वाले solutions कुछ GiB प्रति सेकंड के स्तर पर हैं, और neil का solution 8.4GiB/s तक पहुँचा
    • vmsplice इस्तेमाल करने वाले solutions tkluck के 15.5GiB/s, ais523 के 60.8GiB/s, david के multi-core 208.3GiB/s तक चढ़ते हैं
  • सिर्फ kernel space और user space के बीच copy कम करने के असर से single core के आधार पर करीब 7 गुना फर्क समझाना मुश्किल है
  • अपने प्रयोग में भी ais523 के solution ने 96.4GiB/s, और david के solution ने 7 cores इस्तेमाल करने पर 277GB/s, यानी प्रति core करीब 40GB/s दर्ज किया

baseline: user-space memory write

  • system calls के बिना user-space memory में 32KiB buffer को बार-बार copy करने वाले Rust program ने प्रयोग वाले environment में 167GB/s दर्ज किया
  • इस संख्या को इस्तेमाल किए गए CPU की L1 cache write speed से मेल खाने वाला स्तर माना गया
    • प्रयोग system Ryzen 9 7950X3D, DDR5 6000T/s, Debian 12, Linux 6.1.0-18-amd64 environment था
    • CPU mitigation options को mitigations=off से disable किया गया
  • ftrace profiling में दिखा कि ज्यादातर समय __memset_avx512_unaligned_erms में खर्च हुआ
  • हालांकि corrections इस व्याख्या को सीमित करते हैं
    • रुकने वाले point पर instruction rep stos था, और यह AVX-512 instruction नहीं है
    • AVX2 और SSE2 तक सीमित करने पर भी throughput 167GB/s बने रहने की वजह यह थी कि हर case में rep stos इस्तेमाल हुआ था

write से pipe में लिखते समय वास्तविक लागत

  • उसी size के buffer को stdout.write() से pipe में लिखकर pv >/dev/null से consume करने पर throughput 17GB/s तक गिर जाता है
  • profiling के अनुसार ज्यादातर समय write system call के अंदर खर्च हुआ, खासकर 95% pipe_write के अंदर ही रहा
  • pipe_write में page preparation, locking और copying मिलकर लागत बनाते हैं
    • __alloc_pages: कुल समय का 36%, pipe के लिए नए memory pages तैयार करता है
    • __mutex_lock.constprop.0: कुल समय का 25%, pipe write के लिए locking cost लेता है
    • _raw_spin_lock_irq: कुल समय का 5%, pipe write से जुड़ी locking cost के रूप में दिखा
    • copy_user_enhanced_fast_string: कुल समय का लगभग 20%, user space से kernel की ओर data copy करता है
  • क्योंकि pv, splice के जरिए pages को /dev/null में move करके consume करता है, इसलिए loop में वही कुछ pages लगातार reuse करना मुश्किल है

kernel copy routine और corrected interpretation

  • copy_user_enhanced_fast_string को disassemble करने पर बड़े buffer copies REP MOV instruction से process होते हैं
  • यह function C में नहीं, बल्कि Linux kernel के assembly code में implement है, इसलिए यह compiler optimization छूटने का मामला नहीं बल्कि intentional path है
  • मूल प्रयोग में user space से सीधे rep movsb call करके 80GB/s मिला था, और इसे kernel copy routine के करीब 2 गुना धीमा होने की व्याख्या से जोड़ा गया
  • बाद में correction के साथ experiment conditions बदलीं
    • 32KiB के 2 buffers L1 data cache को saturate कर देते हैं
    • 16KiB buffer इस्तेमाल करने पर performance 153GB/s तक बढ़ती है
  • इसलिए kernel copy में vector instructions न इस्तेमाल करने को बड़ी penalty मानने वाली मूल व्याख्या भरोसेमंद नहीं है
  • फिर भी pipe write में memory management overhead बड़ा है, यह बात बनी रहती है

vmsplice जिन kernel paths से बचता है

  • vmsplice user space के पूरे buffer को kernel में copy किए बिना pipe को दे देता है, जिससे pipe इस्तेमाल करने की लागत कम होती है
  • Francesco के pipes-speed-test में शामिल ./write example को लगातार 'X' लिखने वाले minimal example के रूप में इस्तेमाल किया गया
  • इस example ने 210GB/s दर्ज किया, लेकिन यह वही buffer बार-बार vmsplice को देता है, इसलिए यह सामान्य data generation task जैसा नहीं है
    • अगर यह constant byte stream नहीं है, तो नए data को buffer में भरना पड़ेगा
    • तब simple memory write की upper bound 167GB/s फिर relevant हो जाती है
  • vmsplice path में भी __mutex_lock.constprop.0 में 37% समय खर्च होता है
  • लेकिन write path में दिखे __alloc_pages, _raw_spin_lock_irq, copy_user_enhanced_fast_string दिखाई नहीं देते
  • इसके बजाय add_to_pipe, import_iovec, iov_iter_get_pages2 main paths के रूप में दिखते हैं, जिससे पता चलता है कि vmsplice, write के महंगे हिस्सों को bypass करता है

बचा हुआ निष्कर्ष और ध्यान देने वाली बातें

  • प्रयोग के हिसाब से Linux pipe में write से लिखने वाला path simple memory write की तुलना में लगभग 10 गुना धीमा है
  • मूल निष्कर्ष यह था कि pipe write में locking cost और SIMD context save/restore cost बड़ी होती है, और splicevmsplice इन्हें bypass करते हैं
  • correction के बाद निष्कर्ष को ज्यादा सीमित रूप में देखना चाहिए
    • kernel का memory management overhead अब भी pipe performance degradation का अहम factor है
    • vector instructions न इस्तेमाल करने को उम्मीद जितनी बड़ी penalty मानना गलत है
    • inter-process communication सिर्फ L1 cache से नहीं हो सकता, इसलिए 167GB/s actual pipe reads समेत throughput के रूप में हासिल होना मुश्किल है
  • कुछ गंभीर गलतियां correct की गई हैं और result reliability सीमित हो सकती है, इसलिए आंकड़ों को दिशा समझने के लिए interpret करना चाहिए
  • pipe throughput बढ़ाने के लिए सिर्फ system calls की संख्या घटाना काफी नहीं; write जिन kernel internal paths से गुजरता है और buffer handling तरीका भी साथ में देखना होगा

1 टिप्पणियां

 
GN⁺ 2024-08-27
Hacker News टिप्पणियां
  • इस समस्या से निपटने की कोशिश करने वाला एक side project है: https://lwn.net/Articles/976836/
    आइडिया यह है कि सभी supported file descriptors के लिए ring buffer पाने वाली एक system call बनाई जाए। इसमें pipes भी शामिल होंगे, और अगर दोनों सिरों पर ring buffer इस्तेमाल करने का support हो, तो वही ring buffer map करके zero-copy I/O किया जा सकेगा, और कुछ मामलों में kernel call भी बिल्कुल नहीं करनी पड़ेगी। साथ काम करने वाले लोग ढूंढे जा रहे हैं

    • कम से कम userspace इस्तेमाल के लिए, मुझे नहीं पता कि नया kernel feature सच में जरूरी है या नहीं। मैंने पहले eventfd से pipe behavior की काफी करीबी नकल करने वाला userspace single-producer/single-consumer ring buffer implement किया था
      ring buffer भर जाने या खाली होने पर sleep/poll किया जा सकता है, और बाकी समय यह बिना lock और बिना system call overhead के चलता है
    • मैं सोच रहा हूं कि क्या pipe के दोनों सिरों द्वारा ring buffer support करने की बात दूसरे पक्ष को बताने के लिए कोई standard signaling तरीका plan किया गया है। तभी libc इसे transparently handle कर पाएगा; वरना सिर्फ pipes के लिए, shared memory और futex synchronization की तुलना में इसका क्या फायदा है, यह साफ नहीं है
    • शायद ringbuffer_wait() को भी poll() में readable state बनाकर signal कराया जा सकता है
    • उत्सुकता है कि मौजूदा ring buffer interfaces इसका इस्तेमाल करेंगे या फिर xkcd927 वाली स्थिति बनेगी। फिर भी यह एक दिलचस्प कोशिश लगती है
    • buffering किसी वजह से मौजूद है, और यह तरीका scripts में अजीब failure modes और vulnerabilities पैदा कर सकता है। मुख्य बात यह है कि कोई भी stream producer किसी खास consumer से धीमा हो सकता है
      बस एक पल का hiccup भी हो तो पर्याप्त buffering न होने पर pipe पूरी तरह उलझ सकता है, और जरूरी buffer size हर system में अलग होगा
  • JMP सिर्फ RET क्यों नहीं है, इसकी वजह CONFIG_RETHUNK option है। objdump disassembly में जो दिखता है, वह RET को JMP __x86_return_thunk से replace किए जाने का नतीजा है
    https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
    https://github.com/torvalds/linux/blob/v6.1/arch/x86/lib/ret...
    function की शुरुआत और अंत में NOP instructions ftrace के लिए नहीं, बल्कि ASM_CLAC/ASM_STAC macros से आई हैं। ये macros X86_FEATURE_SMAP detect होने पर runtime में CLAC/STAC instructions से भरने के लिए जगह छोड़ते हैं। दोनों instructions 3 bytes की हैं, इसलिए NOPs की संख्या भी उतनी ही है
    https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
    https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
    https://github.com/torvalds/linux/blob/v6.1/arch/x86/kernel/...

    • ऐसी बातें जानते हुए भी ऐसा शरारती one-off nickname चुनने वाले kernel developers शायद गिने-चुने ही होंगे
  • Linux pipes को “धीमा” कहना Toyota Corolla को “धीमा” कहने जैसा है। जब तक use case extreme न हो, यह पर्याप्त तेज है
    क्या आप car racing कर रहे हैं? और वह भी ऐसी category जिसमें skill से ज्यादा speed महत्वपूर्ण है? तो तेज car खरीद लें। नहीं तो Corolla चलाते रहें

    • यह किसी project code का टुकड़ा नहीं है जो अपनी lifetime में सिर्फ कुछ अरब बार चलेगा, बल्कि यह ऐसा code है जो लाखों से लेकर अरबों computers पर अक्सर इस्तेमाल होता है
      इसलिए बहुत छोटी efficiency improvement भी हो तो उस पर काफी समय लगाकर optimize करना आर्थिक रूप से सही है
    • असल में लेखक के मामले में जिसे धीमा pipe कहा जा रहा है, वह 17GB/s, यानी 130Gbps से ज्यादा की speed से data move कर रहा है
      10 साल से ज्यादा समय से pipes को कई कामों में इस्तेमाल किया है, लेकिन कभी pipe speed bottleneck नहीं बनी; आम तौर पर tar, gzip, find, grep, nc जैसे tools bottleneck रहे होंगे। हालांकि ये tools भी अपने काम की तुलना में काफी तेज हैं
    • एक project है जो raw video decode करने वाला proprietary SDK इस्तेमाल करता है। decoded data को pure RGBA में output किया जाता है, और FFMpeg उसे pipe से पढ़कर standard codec में फिर से encode करता है
      FFMpeg Non-Free SDK को source में शामिल नहीं कर सकता, और pure RGBA को file में store करना हास्यास्पद रूप से impractical है। इसलिए pipe ही एकमात्र तरीका है, और high-throughput pipe की जरूरत का वाजिब कारण है
    • जो चीज हर जगह इस्तेमाल होती है उसे कुछ percent तेज बनाना पूरी तरह worthwhile investment है। भले ही individual tasks बहुत ज्यादा तेज न हों, दुनिया भर में जोड़ने पर भारी मात्रा में power और time बचता है
    • कभी-कभी तेज Corolla ही सच में सबसे अच्छा जवाब होती है
      https://www.toyota.com/grcorolla/
      ये cars engineering और performance के लिहाज से शानदार हैं, और मूल रूप से planned GR Yaris को US market में लाना मुश्किल बनाने वाले rules को bypass करने वाली hack जैसी भी हैं। मुझे लगता है कि इसमें इतना engineering/performance/hacking/market context है कि HN के लोग इसे हल्के में ले सकें। ऊपर से company president भी अब भी खुद drive करते और handle करते हैं
  • मुख्य लेख से अलग बात है, लेकिन आधुनिक CPU पर rep movsb सबसे तेज़ vectorized version जितना ही तेज़ है। क्योंकि CPU इसे accelerate करता है
    kernel function का नाम copy_user_enhanced_fast_string भी इसी ओर इशारा करता है। संबंधित CPU features ERMS (Enhanced Repeat Move String, एक तय length से ऊपर rep movsb को तेज़ करता है) और FSRM (Fast Short Repeat Move String, छोटी copies को भी तेज़ करता है) हैं

    • बस इतना ही नहीं है। rep movsb एक threshold तक तेज़ है, लेकिन उसके बाद सामान्य stores या non-temporal stores ज़्यादा तेज़ होते हैं
      सभी thresholds https://codebrowser.dev/glibc/glibc/sysdeps/x86_64/multiarch... में बताए गए हैं
      और ये values fixed भी नहीं हैं; Noah Goldstein अब भी हर साल इन्हें update कर रहे हैं
    • यह भी देखने लायक है कि Linux ने लेख में इस्तेमाल किए गए kernel 6.1 के बाद x86 copy में ERMS और FSRM का इस्तेमाल करने का तरीका कई बार बदला है। संदर्भ के लिए, मेरी machine पर जिसमें FSRM और ERMS दोनों हैं — हैरानी की बात है कि पहला दूसरे को imply नहीं करता — Linux 6.8 पर साधारण pipe और 32KiB buffer से ही 17GB/s मिलता है
    • छोटे memcpy के लिए, मैं अभी भी इंतज़ार कर रहा हूँ कि rep movsb और rep stosb इतने तेज़ हो जाएँ कि simple C loop version हटाया जा सके
    • तो फिर, जैसे C compiler fixed-length memcpy को inline करता है, variable-length memcpy() को कब inline करेगा, यह जानने की उत्सुकता है
  • AVX512 के बारे में लेख में एक बात नहीं दिखी: xsave/xrstor overhead के अलावा AVX512 काफी power खाता है और CPU frequency scaling trigger करता है। details और यह कितना subtle हो सकता है, इसके लिए [1], [2] देखें
    [1] https://www.intel.com/content/dam/www/central-libraries/us/e...
    [2] https://www.intel.com/content/www/us/en/developer/articles/t...

    • यह सिर्फ कुछ खास Intel CPU models पर लागू होता है
  • inter-process communication के लगभग सभी रूप “धीमे” होते हैं। safety के लिए performance cost चुकाने का चुनाव किया गया है

    • इतनी बड़ी cost देना ज़रूरी नहीं है। pipe बहुत कम चीज़ें provide करता है, इसलिए cost भी बहुत कम होनी चाहिए
      खासकर सबसे तेज़ inter-process communication के long function call से धीमे होने की कोई खास वजह नहीं है
    • pipe safety के लिए नहीं, बल्कि मौजूदा programs के बीच data pass करने के लिए एक optimization के रूप में मौजूद है
  • फिर से Hacker News का hug of death झेल रहा है। WordPress page caching की वजह से पिछली बार से बेहतर है, लेकिन फिर भी page load होने में कुछ seconds लग सकते हैं, कृपया समझें

  • मुझे मूल रूप से यह अच्छी तरह समझ नहीं आया कि splice इतना धीमा क्यों होना चाहिए। vmsplice से धीमा होने की वजह के तौर पर buffer allocation और scalar instructions के इस्तेमाल की बात कही गई, लेकिन यह क्यों ज़रूरी है समझ नहीं आया
    splice को बस vmsplice की तरह फिर से implement क्यों नहीं किया जा सकता? ज़रूर कोई अच्छी वजह होगी, शायद मैं चूक रहा हूँ

  • io_uring इस्तेमाल करने वाला version भी देखना दिलचस्प होगा। kernel के साथ buffer पहले से share करके कुछ copies से बचा जा सकता है, और system call overhead से भी बचा जा सकता है। हालांकि यहाँ बाद वाला लगभग negligible लगता है

  • जिस blog को load होने में करीब 20 seconds लगते हैं, उसके लिए यह काफी bold claim है

    • यह article Hacker News के top तक पहुँच गया है, इसलिए शायद थोड़ी रियायत देनी चाहिए
      article खुद अच्छा लगता है, और अंदर क्या हो रहा है यह सीखने के लिए काफी कुछ है