- Linux में इम्प्लीमेंट किए गए Unix pipes की performance को क्रमिक optimization के ज़रिए विश्लेषित किया गया है
- शुरुआती सरल pipe प्रोग्राम की bandwidth लगभग 3.5GiB/s मापी गई, और profiling व system call बदलावों के जरिए इसे 20 गुना से अधिक बढ़ाने की प्रक्रिया को दिखाया गया है
- vmsplice, splice जैसे Zero-Copy system calls का उपयोग कर अनावश्यक data copy घटाने, page size बढ़ाने जैसी कई optimization तकनीकों को समझाया गया है
- Huge Page के उपयोग और busy loop तकनीक लागू कर bottleneck दूर करते हुए अधिकतम 62.5GiB/s की throughput दर्ज की गई
- pipe, paging, synchronization cost, Zero-Copy जैसे high-performance server और kernel programming के महत्वपूर्ण तत्वों पर insight दी गई है
अवलोकन और परिचय
- यह लेख Linux में Unix pipes कैसे इम्प्लीमेंट होते हैं, और pipe के जरिए data पढ़ने-लिखने वाले test program को खुद लिखते हुए उसकी performance को क्रमशः optimize करने की प्रक्रिया पर केंद्रित है
- शुरुआत में लगभग 3.5GiB/s bandwidth वाले एक साधारण प्रोग्राम से शुरू करके, कई optimizations के बाद लगभग 20 गुना performance improvement हासिल किया गया
- हर चरण का optimization perf tool से की गई profiling के आधार पर तय किया गया, और संबंधित source code GitHub - pipes-speed-test पर उपलब्ध है
- प्रेरणा एक high-performance FizzBuzz program (36GiB/s) को देखकर मिली, जिसने pipe के जरिए data processing speed पर यह खोज शुरू कराई
- C language की बुनियादी समझ हो तो सामग्री समझने में कठिनाई नहीं होगी
pipe performance मापन: पहला धीमा संस्करण
- high-performance FizzBuzz program के उदाहरण execution result से पता चलता है कि pipe के जरिए प्रति सेकंड 36GiB data process किया जा सकता है
- FizzBuzz, L2 cache size (256KiB) के block unit में output देता है ताकि memory access और IO overhead के बीच संतुलन बना रहे
- इस लेख में बनाया गया pipe performance test program भी 256KiB block unit में बार-बार output (read/write) करता है, और measurement के लिए read और write दोनों सिरों को सीधे इम्प्लीमेंट किया गया है
write.cpp वही 256KiB buffer बार-बार लिखता है, और read.cpp 10GiB पढ़कर समाप्त होता है तथा throughput दिखाता है
- test result में pipe के जरिए read/write की गति 3.7GiB/s निकली, जो FizzBuzz की तुलना में 10 गुना धीमी दिखती है
write व्यवहार के bottleneck और आंतरिक संरचना
- perf tool से प्रोग्राम चलाकर call graph ट्रैक करने पर पता चला कि कुल समय का लगभग आधा pipe write यानी
pipe_write चरण में खर्च होता है
pipe_write के भीतर अधिकतर समय memory page copy और allocation (copy_page_from_iter, __alloc_pages) में जाता है
- Linux pipe को ring buffer के रूप में इम्प्लीमेंट किया गया है, और हर entry उस page को refer करती है जिसमें वास्तविक data रखा होता है
- pipe का कुल buffer size स्थिर होता है, और pipe भर जाने पर
write block हो जाता है, जबकि खाली होने पर read block हो जाता है
- C structs (
pipe_inode_info, pipe_buffer) में head और tail क्रमशः write/read position को दर्शाते हैं, और इनमें हर page का offset व length info शामिल रहता है
pipe की read/write logic
pipe_write निम्न क्रम में काम करता है
- अगर pipe भरा है, तो जगह खाली होने तक प्रतीक्षा करता है
- मौजूदा
head पर बची जगह को पहले भरता है
- यदि और जगह चाहिए, तो नया page allocate करता है, buffer में data copy करता है, और
head अपडेट करता है
- सभी operations lock से सुरक्षित होते हैं, इसलिए synchronization overhead उत्पन्न होता है
- read भी इसी संरचना में
tail को आगे बढ़ाते हुए data पढ़ता है और पढ़े गए pages को मुक्त करता है
- मूल रूप से user memory से kernel में, फिर kernel से वापस user space में दो बार copy होती है, जिससे काफी overhead पैदा होता है
Zero-Copy: splice/vmsplice के जरिए optimization
- तेज़ IO के लिए सामान्य तरीका kernel को bypass करना या copies को न्यूनतम करना है
- Linux,
splice और vmsplice system calls के जरिए pipe और user space के बीच data move करते समय copy हटाने का समर्थन देता है
splice: pipe और file descriptor के बीच data move
vmsplice: user memory और pipe के बीच data move
- दोनों system calls में वास्तविक data move किए बिना सिर्फ references को आगे बढ़ाया जा सकता है
- उदाहरण के लिए,
vmsplice का उपयोग करते समय 256KiB buffer को दो हिस्सों में बाँटकर double buffering तरीके से हर आधे हिस्से को बारी-बारी pipe में vmsplice किया जाता है
- व्यवहार में
vmsplice लागू करने पर speed 3 गुना से अधिक बढ़कर लगभग 12.7GiB/s हो जाती है, और read side पर splice लगाने से यह 32.8GiB/s तक और बढ़ती है
page-संबंधित bottleneck और Huge Page का उपयोग
- perf analysis से पता चलता है कि
vmsplice का bottleneck pipe lock (mutex_lock) और page acquisition (iov_iter_get_pages) पर केंद्रित है
iov_iter_get_pages, user memory (virtual address) को वास्तविक physical page में बदलकर pipe के भीतर उसका reference सहेजता है
- Linux paging केवल 4KiB pages तक सीमित नहीं है; architecture के अनुसार 2MiB (huge page) जैसे कई size समर्थित हैं
- Huge Page (जैसे 2MiB) उपयोग करने पर page table management और reference count घटने से page translation overhead काफी कम हो जाता है
- प्रोग्राम में huge page लागू करने पर अधिकतम throughput 51.0GiB/s तक पहुँचती है, यानी लगभग 50% अतिरिक्त वृद्धि
busy loop का उपयोग
- बचा हुआ bottleneck pipe में write space आने की प्रतीक्षा (
wait) और reader को जगाने (wake) जैसी synchronization processing है
SPLICE_F_NONBLOCK option का उपयोग कर, और EAGAIN आने पर busy loop में बार-बार call करके kernel scheduling overhead हटाया गया
- यह तकनीक लागू करने पर अधिकतम throughput 62.5GiB/s तक पहुँचती है, यानी 25% और सुधार
- busy loop CPU resources का 100% उपयोग करती है, लेकिन high-performance servers में यह एक आम pattern है
निष्कर्ष और अन्य बातें
- perf और Linux source code analysis के जरिए step-by-step यह समझाया गया है कि pipe performance को नाटकीय रूप से कैसे बढ़ाया जा सकता है
- pipe, splice, paging, Zero-Copy, synchronization cost जैसे high-performance programming के प्रमुख मुद्दों को वास्तविक उदाहरणों के साथ समझा जा सकता है
- वास्तविक code में buffers को अलग-अलग pages पर allocate कर refcount contention घटाने जैसी अतिरिक्त performance tuning भी लागू की गई है
- test में हर program process को अलग core पर pin (
taskset) करके चलाया गया
- Splice family का design संभावित रूप से जोखिमपूर्ण हो सकता है, और कुछ kernel developers के बीच यह लंबे समय से बहस का विषय रहा है
3 टिप्पणियां
वाह! मज़ेदार है! (क्या बात हो रही है, मुझे तो कुछ भी समझ नहीं आ रहा… )
|
Hacker News राय
Linux pipe-आधारित applications को Windows पर port करने का अनुभव भुलाना मुश्किल है; चूंकि यह POSIX standard है, लगा था performance में बहुत बड़ा अंतर नहीं होगा, लेकिन यह बेहद धीमा निकला। pipe connection का इंतज़ार होने की स्थिति में पूरा Windows लगभग रुक जाने जैसा हो जाता था। कुछ साल बाद Win10 पर C# में वही चीज़ फिर से implement की तो थोड़ा बेहतर था, लेकिन performance gap अब भी काफ़ी शर्मनाक लगा।
सुना है कि पिछले कुछ वर्षों में Windows में AF_UNIX sockets जोड़े गए हैं; Win32 pipes की तुलना में किसकी performance बेहतर है, यह जानने की जिज्ञासा है। मेरा अनुमान है कि AF_UNIX बेहतर होगा।
जब कहा जाता है कि "performance बहुत खराब थी", तो क्या इसका मतलब pipe पहले से connect हो जाने के बाद का I/O है, या connection से पहले की प्रक्रिया? अगर connect होने के बाद भी ऐसा था तो हैरानी होगी, लेकिन अगर connect/disconnect बार-बार होना समस्या थी, तो मान सकता हूँ कि OS ने उसे optimize नहीं किया होगा, क्योंकि वास्तव में इसकी ज़रूरत बहुत कम पड़ती है; इसलिए use case के हिसाब से इसे अलग तरह से देखता हूँ।
मैंने हाल ही में देखा कि Windows पर local TCP की performance pipes से कहीं बेहतर है।
यह याद दिलाता है कि POSIX केवल behavior define करता है, performance नहीं; हर platform और OS की अपनी performance विशेषताएँ होती हैं।
बहुत पहले इसका उल्टा अनुभव हुआ था। pipes नहीं थे, लेकिन जब Linux पर PHP app ने .NET-आधारित SOAP API से बात की, तब .NET implementation की response speed बेहतर थी।
संदर्भ के लिए readv() / writev(), splice(), sendfile(), funopen(), io_buffer() जैसे कई तरीके हैं। splice() pipes और UNIX sockets के बीच large data को zero-copy में भेजने के लिए बहुत शानदार है, लेकिन Linux-only है। data transfer के समय user-space memory allocation, अतिरिक्त buffer management, memcpy(), और iovec traversal के बिना सीधे काम करने का यह सबसे तेज़ तरीका है। BSD परिवार में pipes के लिए readv()/writev() वास्तव में optimal हैं या नहीं, इस पर पुष्टि भी चाही गई है। जो भी हो, इस लेख को बहुत प्रभावशाली बताया गया।
sendfile() file→socket zero-copy तरीके से बहुत उच्च performance देता है, और Linux तथा BSD दोनों में उपलब्ध है; लेकिन यह केवल file→socket को support करता है। sendmsg() सामान्य pipes पर इस्तेमाल नहीं किया जा सकता; यह UNIX domain/INET/अन्य sockets के लिए है। वैसे Linux में sendfile अंदरूनी रूप से splice से implement होने की वजह से, file→block device transfer में भी इसे वास्तव में इस्तेमाल करने का अनुभव रहा है।
Linux में pipes के बीच ultra-fast bulk data transfer के लिए splice() सबसे बेहतरीन है, लेकिन io_uring का सही उपयोग किया जाए तो उससे मिलती-जुलती या उससे भी बेहतर performance की उम्मीद की जा सकती है।
shm_open जैसी shared memory और file descriptor passing की विधि व्यवहार में और तेज़ है, और पूरी तरह portable भी है।
बताया गया कि पिछली HN चर्चाओं में इस लेख पर खूब बहस हुई थी: https://news.ycombinator.com/item?id=31592934 (200 टिप्पणियाँ), https://news.ycombinator.com/item?id=37782493 (105 टिप्पणियाँ)।
इसे सचमुच शानदार लेख बताया गया, और यह भी कि इसका समय-समय पर फिर चर्चा में आना बहुत अच्छा लगता है।
इस बात पर अफसोस जताया गया कि अभी तक कोई टिप्पणी नहीं थी। splice का अधिक उपयोग करना चाहूँगा, लेकिन लेख के अंत में बताए गए security और ABI compatibility issues चिंता पैदा करते हैं। splice आगे भी बना रहेगा या नहीं, और performance सुधारने के लिए default pipes को हमेशा splice इस्तेमाल करने लायक patch करना कितना कठिन होगा, इस पर भी सवाल उठाया गया।
पूछा गया कि क्या आधुनिक Linux में SunOS के Doors जैसी कोई चीज़ है। संदर्भ यह है कि embedded applications में बहुत कम latency के साथ छोटे data exchanges की ज़रूरत है, और AF_UNIX से बेहतर तकनीक खोजी जा रही है।
latency के लिहाज़ से shared memory सबसे तेज़ है, लेकिन tasks को जगाना पड़ता है, आमतौर पर futex का उपयोग करके। Google FUTEX_SWAP system call विकसित कर रहा था, जिससे एक task से दूसरे task को direct handoff संभव होता, लेकिन बाद की स्थिति की जानकारी नहीं है।
'Doors' शब्द बहुत सामान्य है, इसलिए खोजने में कठिनाई होती है; कृपया उसका विवरण दें।
अभी AF_UNIX के साथ सटीक समस्या क्या है? क्या कोई ज़रूरी सुविधा नहीं है, latency अपेक्षा से अधिक है, या server/client socket API structure ही उपयुक्त नहीं बैठता? इस पर अतिरिक्त जानकारी माँगी गई।
संक्षेप में जोड़ा गया कि लेख 2022 में लिखा गया था।