Linux पाइप कितनी तेज़ है? (2022)
(mazzo.li)- Linux पाइप throughput को
perfऔर kernel path analysis से ट्रैक करके, साधारणwrite/readआधारित 3.7GiB/s implementation को अंत में 62.5GiB/s तक बढ़ाया गया - Bottleneck की शुरुआत इस बात से होती है कि पाइप 4KiB page references रखने वाले ring buffer की तरह काम करती है, और copy, page allocation, lock, wait/wakeup की लागत भी साथ में चुकानी पड़ती है
vmspliceऔरspliceuser memory और kernel buffer के बीच copy कम करते हैं, जिससे throughput क्रमशः 12.7GiB/s, 32.8GiB/s तक बढ़ता है- इसके बाद virtual address range को
struct pageमें बदलने की लागत प्रमुख हो जाती है, और 2MiB huge page इस्तेमाल करने पर page table traversal का बोझ घटकर throughput 51.0GiB/s तक पहुंचता है - अंतिम optimization
SPLICE_F_NONBLOCKऔर busy loop throughput को और बढ़ाते हैं, लेकिन ready state का इंतज़ार करते समय CPU core को लगातार occupy रखते हैं
प्रयोग का लक्ष्य और baseline performance
- लक्ष्य यह देखना है कि Linux में Unix pipe कैसे implement होती है, और pipe में data लिखने व पढ़ने वाले test program को चरणों में optimize करना है
- प्रयोग का environment Intel Skylake i7-8550U CPU और Linux 5.17 है; Linux की internal implementation लगातार बदलती रहती है, इसलिए numbers environment के हिसाब से बदल सकते हैं
- शुरुआती बिंदु ऐसा program है जो
writeऔरreadsystem calls से 256KiB buffer को बार-बार pipe में भेजता हैwriteवही 256KiB buffer लगातार लिखता हैreadकुल 10GiB पढ़ता है और throughput print करता है- code
pipes-speed-testमें है
- comparison target के रूप में इस्तेमाल किया गया highly optimized FizzBuzz program,
pvmeasurement में लगभग 36.2GiB/s output करता है- FizzBuzz L2 cache size जितने 256KiB blocks इस्तेमाल करता है
- यह experiment भी वही 256KiB block इस्तेमाल करता है, लेकिन output content compute किए बिना pipe IO की upper limit के करीब value देखना चाहता है
- पहली implementation का result इस प्रकार है
./write | ./read- 3.7GiB/s, 256KiB buffer, 40960 iterations, कुल 10GiB transfer
write और read धीमे क्यों हैं
perf record -gऔरperf reportसे देखने परwriteside के समय का लगभग 47%pipe_writeमें खर्च होता हैpipe_writeके अंदरcopy_page_from_iterऔर__alloc_pagesका बड़ा हिस्सा होता है- data copy
- page allocation
- scheduling
- wait/wakeup
- lock acquire/release
- Linux pipe page references रखने वाला ring buffer है
pipe_inode_infoमेंhead,tail, औरpipe_bufferarray होते हैंpipe_bufferमेंstruct page *page,offset,lenहोते हैं- x86-64 में सामान्य page size 4KiB है
- default ring buffer slot count 16 है, और उदाहरण की 8-slot pipe अधिकतम 32KiB रख सकती है
headwrite end को, औरtailread end को दर्शाता है- pipe भर जाए तो
writeblock होता है - pipe खाली हो तो
readblock होता है
- pipe भर जाए तो
pipe_writeआम तौर पर इस क्रम में आगे बढ़ता है- pipe पहले से भरी हो तो space मिलने तक इंतज़ार करता है
- current
headजिस buffer की ओर इशारा कर रहा है उसमें space हो तो पहले उसे भरता है - empty slot और बचा हुआ data हो तो नया page allocate करके भरता है और
headupdate करता है
pipe_readइसके उलट pages consume करता है, पूरी तरह read हुए pages release करता है औरtailupdate करता है- इस path में हर page दो बार copy होता है
- user memory से kernel में एक बार
- kernel से user memory में फिर एक बार
- copy 4KiB page unit में होती है, और बीच में synchronization व page allocation/freeing मिलते रहते हैं
- उसी machine पर single-thread sequential RAM read लगभग 16GiB/s था, और pipe path की complexity को देखते हुए
write/readका करीब 4 गुना धीमा होना हैरान करने वाला नहीं है
vmsplice और splice से copy कम करना
- fast IO में user memory और kernel buffer के बीच copy cost bottleneck बन सकती है
- Linux pipe से जुड़ी zero-copy movement के लिए system calls देता है
vmsplicepipe में डालने वाले buffer कोstruct iovecarray से specify करता है- return value pipe में असल में गए bytes की संख्या है
- pipe ring buffer size limit के कारण requested पूरा size एक बार में नहीं जा सकता
vmspliceuser memory को copy किए बिना pipe से जोड़ता है, इसलिए read side द्वारा उस data को consume करने से पहले buffer reuse न हो, इसका ध्यान रखना जरूरी है- FizzBuzz program इसके लिए double buffering इस्तेमाल करता है
- 256KiB buffer को दो 128KiB halves में बांटता है
- pipe size 128KiB सेट करके 4KiB pages के आधार पर 32 slots बनाता है
- दोनों half buffers को बारी-बारी से भरता है और
vmspliceसे pipe में डालता है
- test program असल में buffer contents दोबारा नहीं लिखता, लेकिन real output generation program के लिए जरूरी structure जैसा double buffering बनाए रखता है
writeकोvmspliceसे बदलने पर throughput 12.7GiB/s हो जाता है- copy amount आधा हो जाता है
write/readकी तुलना में 3x से अधिक improvement
- read side को भी
spliceसे बदलने पर सभी copies हट जाती हैं और throughput 32.8GiB/s तक पहुंचता है - हालांकि,
vmspliceकी safety पर ध्यान देना जरूरी है- page फिर splice होने पर lifetime बढ़ सकती है
SPLICE_F_GIFTके बिना भी यह unsafe है या नहीं, यह स्पष्ट नहीं है- zero-copy pipe को safely इस्तेमाल करने के लिए खास सावधानी चाहिए
iov_iter_get_pages और page conversion cost
vmspliceऔरspliceलागू करने के बादperfमेंvmsplicepath का time बड़ा दिखता हैiov_iter_get_pages__mutex_lock.constprop.0add_to_pipe
iov_iter_get_pages,vmspliceको दिए गएstruct iovecकी virtual memory range को pipe द्वारा रखी जा सकने वालीstruct pagelist में बदलता है- process physical memory address नहीं बल्कि virtual address इस्तेमाल करता है
- CPU page table के जरिए virtual address को physical address में translate करता है
- x86-64 में सामान्य page size 4KiB है
- x86-64 page table को 4-level, हर level पर 512-way tree structure के रूप में समझाया जाता है
- pipe का
pipe_bufferstruct pageको reference करता हैstruct pagekernel की physical page और संबंधित metadata handle करने वाली core structure है- इसलिए
vmspliceको input virtual memory range को physical page references में बदलना पड़ता है
iov_iter_get_pagesके अंदर अधिकांश timeget_user_pages_fastमें खर्च होता है- 128KiB buffer और 4KiB page के आधार पर कुल 32 pages लेने पड़ते हैं
- pipe code implementation में एक बार में
nr_pages = 16के साथ call करता है और जरूरत हो तो repeat करता है, लेकिन कुल spliced page count 32 है
get_user_pages_fastCPU जैसा काम software में करते हुए page table walk करकेstruct pagecollect करता है- caller जब pages इस्तेमाल कर रहा हो, तब physical page reuse न हो, इसके लिए
struct pageका reference count बढ़ाना पड़ता है- बाद में
put_pageसे reference count घटाना पड़ता है
- बाद में
- शुरुआत में
memsetसे buffer भरने पर page table entries पहले से बन जाती हैं औरget_user_pages_fastके slow path से बचा जा सकता हैmemsetन करने पर उदाहरण में throughput 25.0GiB/s तक घट जाता है- huge page इस्तेमाल करने पर यह phenomenon उसी तरह नहीं दिखता
huge page से page handling cost घटाना
- x86-64, 4KiB normal pages के अलावा 2MiB और 1GiB huge pages support करता है
- आगे के experiments सिर्फ 2MiB huge page देखते हैं
- 1GiB pages relatively rare हैं और इस काम के लिए overkill माने गए
- huge page same memory range को fewer pages से represent करके management cost घटाता है
- virtual address को physical address में translate करते समय page table level भी एक कम हो जाता है
- CPU के TLB burden को कम करने में भी मदद कर सकता है
- इस experiment का direct bottleneck hardware page table walk नहीं, बल्कि kernel का software path
get_user_pages_fastहै - Linux में 2MiB-aligned memory allocate करके और
madvise(..., MADV_HUGEPAGE)call करके huge page use करने का अनुरोध किया जा सकता है - huge page लागू करने पर throughput 51.0GiB/s तक बढ़ता है
- performance improvement का कारण यह नहीं है कि
struct pageसीधे एक 2MiB page को point करता है- kernel code आम तौर पर मानता है कि
struct pagecurrent architecture के standard page size को point करता है - huge page को head
struct pageऔर कई tailstruct pageसे represent किया जाता है - 2MiB huge page को 4KiB के आधार पर अधिकतम 512
struct pageसे represent किया जा सकता है
- kernel code आम तौर पर मानता है कि
- फिर भी पहली entry मिल जाने के बाद, आगे के
struct pagesimple loop से generate किए जा सकते हैं, जिससे page table को बार-बार traverse करने की cost घटती है - Linux 5.17 के बाद kernels में head page को explicit रूप से identify करने वाला
struct folioशामिल है, जिससे runtime head/tail checks की जरूरत कम करने वाला improvement है
busy loop से synchronization cost घटाना
- huge page लागू करने के बाद
perfमेंwait_for_spaceऔर__wake_up_common_locktime प्रमुख दिखता है- writable space का इंतज़ार करने की cost
- read side को wake up करने की cost
- इस synchronization cost से बचने के लिए
vmspliceऔरspliceमेंSPLICE_F_NONBLOCKइस्तेमाल किया जा सकता है- pipe में लिखा न जा सके तो तुरंत
EAGAINreturn करता है - caller तैयार होने तक busy loop चलाता है
- pipe में लिखा न जा सके तो तुरंत
- busy loop लागू करने पर throughput 62.5GiB/s तक बढ़ता है
- cost भी स्पष्ट है
vmspliceयाspliceready होने का इंतज़ार करते समय CPU core पूरी तरह occupy रहता है- यह ज्यादा CPU usage के बदले latency या throughput पाने का तरीका है
- अंत में यह synthetic benchmark लगभग 3.5GiB/s से लगभग 65GiB/s level तक improve हुआ
बचे हुए details और practical topics
- optimization process
perfoutput और Linux source code को साथ देखकर आगे बढ़ा - जिन topics को cover किया गया वे pipe और splicing से भी बड़े high-performance programming topics से जुड़े हैं
-
zero-copy operations
- ring buffer
- paging और virtual memory
- synchronization overhead
- real code में page table contention घटाने के लिए दो buffers अलग-अलग allocate किए जाते हैं
get_user_pagespage table entries का reference count बढ़ाता है औरput_pageघटाता है- अगर दो buffers अलग-अलग page table entries इस्तेमाल करें तो reference count modification contention घटता है
- test को
tasksetसे./writeऔर./readprocesses को दो cores पर pin करके run किया गया - repository में
get_user_pages_fastके लिए synthetic benchmark भी शामिल है - huge page use करने या न करने के हिसाब से speed difference measure किया जा सकता है
- splicing अब भी एक ambiguous और risky concept बना हुआ है, और related issues kernel developers पर लगातार बोझ बने रहते हैं
-
1 टिप्पणियां
Hacker News की राय
अगर मैंने ठीक समझा है, तो
vmspliceजब पढ़ने और लिखने वाले दोनों सिरों पर साथ-साथ इस्तेमाल किया जाए, तो यह दो processes के बीच छोटे shared memory mechanism जैसा दिखता हैयानी दोनों processes को बेहद सावधान रहना पड़ता है कि buffer कब पढ़ना/लिखना है और इस्तेमाल के बाद उसे कैसे लौटाना है। यह तेज़ है, लेकिन साथ ही डरावना तरीका भी है, और यह बात भी अफ़सोसजनक है कि जो सीधा-सादा implementation हर कोई लिखेगा, वह संभव performance से 20 गुना धीमा है
vmspliceदो processes के बीच छोटा shared memory mechanism है। यह सिर्फ़ user memory से pipe तक zero-copy support करता है, और उलटी दिशा में copy होती हैअधिक जानकारी के लिए https://mazzo.li/posts/fast-pipes.html#fn10 देखें
मैं सोच रहा हूँ कि क्या कोई data processing library है जो pipes, sockets, files और memory को abstract करते हुए ऐसी optimizations भी कर देती हो
जानना चाहता हूँ कि C, C++, Rust या किसी दूसरी systems language में ऐसी कोई library है या नहीं। लेख में आए
splice()औरvmsplice()जैसे APIs से मैं परिचित नहीं हूँ, इसलिए low-level applications बनाते समय सोचने लगा कि क्या कोई library होगी जो जहाँ संभव हो वहाँ ऐसी optimizations अपने-आप इस्तेमाल कर ले। यह भी जानना चाहूँगा किlibuv,tokio,NettyLinux पर इसे automatically handle करते हैं या नहीं; थोड़ी-सी खोज से लगा कि शायद ऐसा हो सकता हैउच्च-स्तरीय code ऐसे features बहुत कम इस्तेमाल करता है, क्योंकि ये काफ़ी special-purpose हैं और Linux के हिसाब से specialize करने पड़ते हैं। अगर आप data को देखे बिना सिर्फ़ Linux पर move कर रहे हैं, तो
spliceउपयोगी है। TCP/UDP proxy जैसे applications के लिए यह निश्चित रूप से चाहिए, लेकिन साधारण HTTP server के लिए यह उतना fit नहीं है। अगर आप ऐसा app बना रहे हैं, तो zero copy जैसे keywords आपको बार-बार मिलेंगे, औरspliceसबसे पहले दिखने वाले results में से एक होगाtokioके लिए एक crate है। automatic नहीं है, लेकिन दिलचस्प हो सकता है: https://lib.rs/crates/tokio-spliceresearch environment के बाहर यह बहुत प्रसिद्ध नहीं लगता, और efficient Cosh implementation लिखने में काफ़ी समय लग सकता है। संक्षेप में, इसके तीन transfer modes हैं: move, share, copy। उदाहरण के लिए, move transfer में sender अपने read/write अधिकार वाले data को receiver को पूरी तरह सौंप देता है, और इसे page table की virtual memory remapping से implement किया जा सकता है। साथ ही strong/weak properties भी हैं, जो बताती हैं कि क्या sender और receiver के सहयोग करने पर भरोसा किया जा सकता है, या virtual memory permission remapping से सख़्त isolation करना होगा। सच कहूँ तो, मुझे नहीं पता कि इसे इतना optimize किया जा सकता है कि यह बहुत ज़्यादा optimized pipe जैसी चीज़ से reliably टक्कर ले सके; यह “काफ़ी smart compiler” वाली समस्या भी बन सकती है। फिर भी मुझे लगता है कि इसे आज़माने लायक है
[1] https://barrelfish.org/publications/trios14-baumann-cosh.pdf
2022 से पहले की चर्चा: https://news.ycombinator.com/item?id=31592934
4 साल पहले संयोग से पता चली एक हैरान करने वाली बात यह थी कि Linux pipes इस्तेमाल करने पर nondeterministic behavior हो सकता है
https://www.gibney.org/the_output_of_linux_pipes_can_be_inde...
echoकिए गए data में से कुछ भी pass नहीं करती(echo red; echo green 1>&2) | echo blue|symbol के दोनों तरफ दो subshell बनाता है। subshell मौजूदा shell के child process होते हैं, इसलिए वे open file descriptor table जैसी अहम properties inherit करते हैं। दोनों subshell साथ-साथ चलते हैं, और parent shell बस सभी child processes के खत्म होने काwait()करता है। कौन-सा child पहले चलेगा, यह आम तौर पर predict नहीं किया जा सकता, और multicore system पर वे सचमुच एक साथ भी चल सकते हैं। बाएँ subshell का standard output pipe के write end से, और दाएँ subshell का standard input read end से जुड़ा होता है। लेकिनecho blueinput पढ़ता नहीं, सिर्फ output करता है, इसलिए pipe से कुछ भी read नहीं होता।echo green >&2standard output को pipe में नहीं, बल्कि जहाँ standard error point कर रहा है वहाँ भेजता है। आखिर मेंecho greenऔरecho blueएक ही file, शायद terminal, में सीधे लिखते हैं, जिससे race condition बनती है, और order इस पर निर्भर करता है कि किसे पहले schedule किया गयावरना pipeline उपयोगी नहीं होगी। उदाहरण के लिए,
curlसे tar file download करके तुरंत extract करने वाली pipeline में, अगरcurlखत्म होने तक इंतज़ार करके फिरtarचलाएँ, तो बड़ी intermediate tar file कहाँ store करें जैसी समस्या आएगी। buffer छोटा रखने और तेज चलने के लिएtarकोcurlके चलने के दौरान ही साथ चलना चाहिए। pipeline programs के बीच इकलौता control flow standard input और standard output से बनता है। उदाहरण में standard error पर लिखा जा रहा है, इसलिए वह जाहिर तौर पर deterministic control flow में शामिल नहीं हैसाथ ही, confusion से बचने के लिए: “Indeterministic” दर्शन का term है, और computer science का term “nondeterministic” है
0. https://blog.superpat.com/zero-copy-in-linux-with-sendfile-a...
यह command शायद जानबूझकर अजीब बनाई गई है, और code reviewer होता तो निश्चित ही शक करता।
echo redहै, लेकिन कहीं pass नहीं होता। यह “red herring” joke भी हो सकता है।echo greenstandard error पर जाता है, इसलिए वह तभी दिखता है जबecho blueसे पहले खत्म हो। exact order output buffering पर निर्भर करता है, और यह इस बात पर बदलता है कि कौन-सा time slice पहले assign हुआ, साथ ही CPU की संख्या और load पर भी। इसलिए यह nondeterministic है, लेकिन उसी तरह जैसेtopnondeterministic होता हैसंक्षेप में, अगर मान लें कि दोनों programs को जितना संभव हो उतना optimal लिखा गया है, तो pipe की maximum speed system के एक core की read/write speed के करीब होती है
मूल रूप से kernel एक program के standard output से दूसरे program के standard input में वही physical memory pages map करता है, इसलिए काम zero-copy हो जाता है, या कम optimal हालात में तेज single copy के करीब। यह जान लेने के बाद, shell scripts में pipe से दो या ज्यादा tools जोड़कर बेहद high-performance काम करना rewarding भी लगता है और थोड़ा मजेदार भी। यह toolbox के सबसे उपयोगी tools में से एक है
spliceयाvmspliceइस्तेमाल किए जाएँ। ये Linux-only system calls लिखने में कठिन हैं, खासकरvmspliceज्यादातर programs और shell filters,
pvजैसे notable exceptions को छोड़कर, इन्हें इस्तेमाल नहीं करते, इसलिए kernel memory में copy करने और फिर वापस निकालने की cost चुकाते हैंआप जो application बना रहे हैं, उसके load characteristics के हिसाब से pipe+process या green/user-space threads से ज्यादा साफ तरीके से implement हो सकती है। कम convenient हो सकता है, लेकिन message passing आम तौर पर deadlock hell से बेहतर होती है
यह लेख Linux pipes को तेज बनाने के तरीके पर है, लेकिन shared memory या message queue जैसे दूसरे तरीके अब भी तेज हो सकते हैं
जिन systems में बहुत सारा data तेजी से move करना होता है, वहाँ pipe का extra step speed कम कर सकता है। जब कई threads data share करते हैं, तब भी pipe दूसरे तरीकों की तुलना में ज्यादा problems पैदा कर सकती है। इसलिए लेख में बताए गए improvements शायद speed-critical real-world situations में बहुत मददगार न हों
io_uringजैसी चीज़ चुनने पर फायदा होता हैलेकिन bidirectional communication में किसी न किसी तरफ data ready होने की notification चाहिए। हो सकता है आप CPU जलाते हुए polling न करना चाहें, और मुझे साफ नहीं कि ऐसे options वह synchronization pipe से तेज कैसे handle करते हैं
page table जैसी चीज़ों के बारे में पता था, लेकिन इसे
perfके जरिए performance analysis से जोड़कर देखने पर साफ हुआ कि throughput में यह कितना central हैpipes बेहतरीन हैं। दूसरा process किसी दूसरे CPU पर हो या दूसरी machine पर, सच कहूँ तो खास फर्क नहीं पड़ता
https://github.com/nathants/s4/blob/master/examples/nyc_taxi...
pipes
cat,sed,awk,cut,grep,uniq,jqआदि को बार-बार combine करने के लिए पर्याप्त तेज हैं