Linux पाइप की परफॉर्मेंस में गिरावट
(qsantos.fr)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 की लागत मिलकर जमा होती है vmspliceuser-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 तक पहुँचा, और एक अलग प्रयोग केvmspliceexample ने 210GB/s दर्ज किया - corrections की वजह से SIMD न इस्तेमाल करने की penalty वाली व्याख्या भरोसेमंद नहीं है, और inter-process communication सिर्फ L1 cache में पूरा नहीं होता, इसलिए 167GB/s को वास्तविक pipe throughput मानना मुश्किल है
शुरुआती बिंदु: vmsplice से बनने वाला बड़ा अंतर
- कुछ programs pipe के जरिए data को ज्यादा तेजी से move करने के लिए
vmsplicesystem 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 किया गया
ftraceprofiling में दिखा कि ज्यादातर समय__memset_avx512_unaligned_ermsमें खर्च हुआ- हालांकि corrections इस व्याख्या को सीमित करते हैं
- रुकने वाले point पर instruction
rep stosथा, और यह AVX-512 instruction नहीं है - AVX2 और SSE2 तक सीमित करने पर भी throughput 167GB/s बने रहने की वजह यह थी कि हर case में
rep stosइस्तेमाल हुआ था
- रुकने वाले point पर instruction
write से pipe में लिखते समय वास्तविक लागत
- उसी size के buffer को
stdout.write()से pipe में लिखकरpv >/dev/nullसे consume करने पर throughput 17GB/s तक गिर जाता है - profiling के अनुसार ज्यादातर समय
writesystem 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 copiesREP MOVinstruction से process होते हैं- यह function C में नहीं, बल्कि Linux kernel के assembly code में implement है, इसलिए यह compiler optimization छूटने का मामला नहीं बल्कि intentional path है
- मूल प्रयोग में user space से सीधे
rep movsbcall करके 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 से बचता है
vmspliceuser space के पूरे buffer को kernel में copy किए बिना pipe को दे देता है, जिससे pipe इस्तेमाल करने की लागत कम होती है- Francesco के
pipes-speed-testमें शामिल./writeexample को लगातार'X'लिखने वाले minimal example के रूप में इस्तेमाल किया गया - इस example ने 210GB/s दर्ज किया, लेकिन यह वही buffer बार-बार
vmspliceको देता है, इसलिए यह सामान्य data generation task जैसा नहीं है- अगर यह constant byte stream नहीं है, तो नए data को buffer में भरना पड़ेगा
- तब simple memory write की upper bound 167GB/s फिर relevant हो जाती है
vmsplicepath में भी__mutex_lock.constprop.0में 37% समय खर्च होता है- लेकिन
writepath में दिखे__alloc_pages,_raw_spin_lock_irq,copy_user_enhanced_fast_stringदिखाई नहीं देते - इसके बजाय
add_to_pipe,import_iovec,iov_iter_get_pages2main paths के रूप में दिखते हैं, जिससे पता चलता है किvmsplice,writeके महंगे हिस्सों को bypass करता है
बचा हुआ निष्कर्ष और ध्यान देने वाली बातें
- प्रयोग के हिसाब से Linux pipe में
writeसे लिखने वाला path simple memory write की तुलना में लगभग 10 गुना धीमा है - मूल निष्कर्ष यह था कि pipe write में locking cost और SIMD context save/restore cost बड़ी होती है, और
spliceवvmspliceइन्हें 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 टिप्पणियां
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 भी बिल्कुल नहीं करनी पड़ेगी। साथ काम करने वाले लोग ढूंढे जा रहे हैं
ring buffer भर जाने या खाली होने पर sleep/poll किया जा सकता है, और बाकी समय यह बिना lock और बिना system call overhead के चलता है
ringbuffer_wait()को भीpoll()में readable state बनाकर signal कराया जा सकता हैबस एक पल का hiccup भी हो तो पर्याप्त buffering न होने पर pipe पूरी तरह उलझ सकता है, और जरूरी buffer size हर system में अलग होगा
JMPसिर्फRETक्यों नहीं है, इसकी वजह CONFIG_RETHUNK option है।objdumpdisassembly में जो दिखता है, वह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/...
Linux pipes को “धीमा” कहना Toyota Corolla को “धीमा” कहने जैसा है। जब तक use case extreme न हो, यह पर्याप्त तेज है
क्या आप car racing कर रहे हैं? और वह भी ऐसी category जिसमें skill से ज्यादा speed महत्वपूर्ण है? तो तेज car खरीद लें। नहीं तो Corolla चलाते रहें
इसलिए बहुत छोटी efficiency improvement भी हो तो उस पर काफी समय लगाकर optimize करना आर्थिक रूप से सही है
10 साल से ज्यादा समय से pipes को कई कामों में इस्तेमाल किया है, लेकिन कभी pipe speed bottleneck नहीं बनी; आम तौर पर tar, gzip, find, grep, nc जैसे tools bottleneck रहे होंगे। हालांकि ये tools भी अपने काम की तुलना में काफी तेज हैं
FFMpeg Non-Free SDK को source में शामिल नहीं कर सकता, और pure RGBA को file में store करना हास्यास्पद रूप से impractical है। इसलिए pipe ही एकमात्र तरीका है, और high-throughput pipe की जरूरत का वाजिब कारण है
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 कर रहे हैं
memcpyके लिए, मैं अभी भी इंतज़ार कर रहा हूँ किrep movsbऔरrep stosbइतने तेज़ हो जाएँ कि simple C loop version हटाया जा सकेmemcpyको inline करता है, variable-lengthmemcpy()को कब inline करेगा, यह जानने की उत्सुकता हैAVX512 के बारे में लेख में एक बात नहीं दिखी:
xsave/xrstoroverhead के अलावा 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...
inter-process communication के लगभग सभी रूप “धीमे” होते हैं। safety के लिए performance cost चुकाने का चुनाव किया गया है
खासकर सबसे तेज़ inter-process communication के long function call से धीमे होने की कोई खास वजह नहीं है
फिर से Hacker News का hug of death झेल रहा है। WordPress page caching की वजह से पिछली बार से बेहतर है, लेकिन फिर भी page load होने में कुछ seconds लग सकते हैं, कृपया समझें
मुझे मूल रूप से यह अच्छी तरह समझ नहीं आया कि
spliceइतना धीमा क्यों होना चाहिए।vmspliceसे धीमा होने की वजह के तौर पर buffer allocation और scalar instructions के इस्तेमाल की बात कही गई, लेकिन यह क्यों ज़रूरी है समझ नहीं आयाspliceको बसvmspliceकी तरह फिर से implement क्यों नहीं किया जा सकता? ज़रूर कोई अच्छी वजह होगी, शायद मैं चूक रहा हूँvmspliceहर तरह के file descriptors पर काम नहीं करताio_uring इस्तेमाल करने वाला version भी देखना दिलचस्प होगा। kernel के साथ buffer पहले से share करके कुछ copies से बचा जा सकता है, और system call overhead से भी बचा जा सकता है। हालांकि यहाँ बाद वाला लगभग negligible लगता है
जिस blog को load होने में करीब 20 seconds लगते हैं, उसके लिए यह काफी bold claim है
article खुद अच्छा लगता है, और अंदर क्या हो रहा है यह सीखने के लिए काफी कुछ है