1 คะแนน โดย GN⁺ 2023-10-06 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ติดตาม throughput ของ Linux pipe ด้วย perf และการวิเคราะห์ path ใน kernel จาก implementation แบบง่ายที่ใช้ write/read ที่ 3.7GiB/s จนปรับขึ้นเป็น 62.5GiB/s ในท้ายที่สุด
  • จุดเริ่มต้นของคอขวดอยู่ที่ pipe ทำงานเป็น ring buffer ที่เก็บ reference ของหน้า 4KiB และต้องจ่ายต้นทุนทั้งการคัดลอก การจัดสรรหน้า lock และการรอ·ปลุก
  • vmsplice และ splice ลดการคัดลอกระหว่างหน่วยความจำผู้ใช้กับ kernel buffer ทำให้ throughput เพิ่มขึ้นเป็นประมาณ 12.7GiB/s และ 32.8GiB/s ตามลำดับ
  • หลังจากนั้น ต้นทุนในการแปลงช่วง virtual address เป็น struct page เด่นชัดขึ้น และเมื่อใช้ 2MiB huge page ภาระการไล่ page table ลดลง จึงเพิ่มขึ้นเป็น 51.0GiB/s
  • การ optimize ขั้นสุดท้ายอย่าง SPLICE_F_NONBLOCK และ busy loop ทำให้ throughput สูงขึ้นอีก แต่จะยึดครอง CPU core ตลอดเวลาระหว่างรอสถานะพร้อม

เป้าหมายการทดลองและ performance อ้างอิง

  • เป้าหมายคือดูว่า Unix pipe บน Linux ถูก implement อย่างไร และค่อย ๆ optimize โปรแกรมทดสอบที่เขียนและอ่านข้อมูลผ่าน pipe ทีละขั้น
  • สภาพแวดล้อมการทดลองคือ CPU Intel Skylake i7-8550U และ Linux 5.17 โดย implementation ภายในของ Linux เปลี่ยนแปลงอยู่เสมอ ดังนั้นตัวเลขอาจต่างกันตามสภาพแวดล้อม
  • จุดเริ่มต้นคือโปรแกรมที่ใช้ system call write และ read เพื่อส่ง buffer ขนาด 256KiB ผ่าน pipe ซ้ำ ๆ
    • write เขียน buffer ขนาด 256KiB เดิมซ้ำไปเรื่อย ๆ
    • read อ่านรวม 10GiB แล้วพิมพ์ throughput
    • โค้ดอยู่ที่ pipes-speed-test
  • โปรแกรม FizzBuzz ที่ optimize อย่างมากซึ่งใช้เป็นตัวเปรียบเทียบ ให้ output ประมาณ 36.2GiB/s เมื่อวัดด้วย pv
    • FizzBuzz ใช้ block ขนาด 256KiB ซึ่งเท่ากับขนาด L2 cache
    • การทดลองนี้ก็ใช้ block 256KiB เช่นกัน แต่ไม่ได้คำนวณเนื้อหา output เพื่อดูค่าที่ใกล้ขีดจำกัดของ pipe IO
  • ผลของ implementation แรกเป็นดังนี้
    • ./write | ./read
    • 3.7GiB/s, buffer 256KiB, ทำซ้ำ 40960 ครั้ง, ส่งข้อมูลรวม 10GiB

เหตุผลที่ write และ read ช้า

  • เมื่อตรวจด้วย perf record -g และ perf report พบว่าเวลาฝั่ง write ประมาณ 47% ถูกใช้ใน pipe_write
  • ภายใน pipe_write นั้น copy_page_from_iter และ __alloc_pages มีสัดส่วนสูง
    • การคัดลอกข้อมูล
    • การจัดสรรหน้า
    • scheduling
    • การรอ·ปลุก
    • การ acquire/release lock
  • Linux pipe คือ ring buffer ที่เก็บ reference ของหน้า
    • pipe_inode_info มี head, tail และ array pipe_buffer
    • pipe_buffer มี struct page *page, offset, len
    • ขนาดหน้าแบบปกติของ x86-64 คือ 4KiB
    • จำนวน slot ของ ring buffer โดยค่าเริ่มต้นคือ 16 slot และ pipe 8 slot ในตัวอย่างสามารถเก็บได้สูงสุด 32KiB
  • head แทนปลายเขียน และ tail แทนปลายอ่าน
    • ถ้า pipe เต็ม write จะถูก block
    • ถ้า pipe ว่าง read จะถูก block
  • โดยทั่วไป pipe_write ทำงานตามลำดับดังนี้
    • ถ้า pipe เต็มอยู่แล้ว ให้รอจนกว่าจะมีพื้นที่ว่าง
    • ถ้า buffer ที่ head ปัจจุบันชี้อยู่ยังมีพื้นที่ ให้เติมก่อน
    • ถ้ามี slot ว่างและยังมีข้อมูลเหลือ ให้จัดสรรหน้าใหม่ เติมข้อมูล และอัปเดต head
  • pipe_read ทำงานกลับกัน คือ consume หน้า, free หน้าที่อ่านหมดแล้ว และอัปเดต tail
  • ใน path นี้ แต่ละหน้าถูกคัดลอกสองครั้ง
    • ครั้งหนึ่งจากหน่วยความจำผู้ใช้เข้า kernel
    • อีกครั้งจาก kernel กลับไปยังหน่วยความจำผู้ใช้
  • การคัดลอกเกิดเป็นหน่วยหน้า 4KiB และมีการ sync กับการจัดสรร·free หน้าแทรกอยู่ระหว่างทาง
  • บนเครื่องเดียวกัน การอ่าน RAM แบบ sequential ด้วย thread เดียวได้ประมาณ 16GiB/s และเมื่อคำนึงถึงความซับซ้อนของ path ของ pipe แล้ว การที่ write/read ช้ากว่าประมาณ 4 เท่าก็ไม่น่าแปลกใจ

ลดการคัดลอกด้วย vmsplice และ splice

  • ใน IO ความเร็วสูง ต้นทุนการคัดลอก ระหว่างหน่วยความจำผู้ใช้กับ kernel buffer อาจกลายเป็นคอขวดได้
  • Linux มี system call สำหรับการย้ายข้อมูลแบบไม่คัดลอกที่เกี่ยวข้องกับ pipe
    • splice: ย้ายข้อมูลระหว่าง pipe กับ file descriptor
    • vmsplice: ย้ายข้อมูลจากหน่วยความจำผู้ใช้เข้า pipe
  • vmsplice ระบุ buffer ที่จะใส่เข้า pipe ด้วย array ของ struct iovec
    • ค่าที่ return คือจำนวน byte ที่เข้า pipe จริง
    • เนื่องจากข้อจำกัดของขนาด ring buffer ของ pipe ขนาดที่ขอทั้งหมดอาจไม่เข้าไปในครั้งเดียว
  • vmsplice เชื่อมหน่วยความจำผู้ใช้เข้ากับ pipe โดยไม่คัดลอก ดังนั้นต้องระวังอย่า reuse buffer ก่อนที่ฝั่งอ่านจะ consume ข้อมูลนั้น
  • โปรแกรม FizzBuzz ใช้ double buffering เพื่อเรื่องนี้
    • แบ่ง buffer 256KiB เป็นสองครึ่ง ครึ่งละ 128KiB
    • ตั้งขนาด pipe เป็น 128KiB เพื่อให้มี 32 slot เมื่อคิดตามหน้า 4KiB
    • เติม buffer สองครึ่งสลับกัน แล้วใส่เข้า pipe ด้วย vmsplice
  • โปรแกรมทดสอบนี้ไม่ได้เขียนเนื้อหา buffer ซ้ำจริง ๆ แต่ยังคงโครงสร้าง double buffering ไว้คล้ายกับที่โปรแกรมสร้าง output จริงต้องใช้
  • เมื่อเปลี่ยน write เป็น vmsplice throughput เพิ่มเป็น 12.7GiB/s
    • ปริมาณการคัดลอกลดลงครึ่งหนึ่ง
    • ดีขึ้นมากกว่า 3 เท่าเมื่อเทียบกับ write/read
  • เมื่อเปลี่ยนฝั่งอ่านเป็น splice ด้วย จะตัดการคัดลอกทั้งหมดออกและเพิ่มขึ้นถึง 32.8GiB/s
  • อย่างไรก็ตาม ต้องระวังเรื่องความปลอดภัยของ vmsplice
    • ถ้าหน้าถูก splice อีกครั้ง อายุการใช้งานอาจยืดออกไป
    • ยังไม่ชัดเจนว่าปลอดภัยหรือไม่แม้ไม่มี SPLICE_F_GIFT
    • การใช้ pipe แบบ zero-copy อย่างปลอดภัยต้องระวังเป็นพิเศษ

iov_iter_get_pages และต้นทุนการแปลงหน้า

  • หลังใช้ vmsplice และ splice แล้ว ใน perf เวลาของ path vmsplice ดูสูงขึ้นอย่างชัดเจน
    • iov_iter_get_pages
    • __mutex_lock.constprop.0
    • add_to_pipe
  • iov_iter_get_pages แปลง ช่วง virtual memory ของ struct iovec ที่ส่งให้ vmsplice เป็นรายการ struct page ที่ pipe สามารถเก็บได้
  • process ใช้ virtual address ไม่ใช่ physical memory address
    • CPU แปลง virtual address เป็น physical address ผ่าน page table
    • ขนาดหน้าปกติของ x86-64 คือ 4KiB
    • page table ของ x86-64 อธิบายได้ว่าเป็นโครงสร้าง tree 4 ระดับ โดยแต่ละระดับเป็น 512-way
  • pipe_buffer ของ pipe อ้างอิง struct page
    • struct page เป็นโครงสร้างหลักที่ kernel ใช้จัดการ physical page และ metadata ที่เกี่ยวข้อง
    • ดังนั้น vmsplice ต้องแปลงช่วง virtual memory ที่ป้อนเข้ามาเป็น reference ของ physical page
  • เวลาภายใน iov_iter_get_pages ส่วนใหญ่ถูกใช้ใน get_user_pages_fast
    • สำหรับ buffer 128KiB และหน้า 4KiB ต้องได้หน้าทั้งหมด 32 หน้า
    • ด้วย implementation ของโค้ด pipe จะเรียกครั้งละ nr_pages = 16 และทำซ้ำหากจำเป็น แต่จำนวนหน้า spliced ทั้งหมดคือ 32 หน้า
  • get_user_pages_fast เดิน page table ด้วย software คล้ายกับที่ CPU ทำ เพื่อรวบรวม struct page
  • ต้องเพิ่ม reference count ของ struct page เพื่อไม่ให้ physical page ถูก reuse ระหว่างที่ caller ใช้หน้าอยู่
    • หลังจากนั้นต้องลด reference count ด้วย put_page
  • ถ้าเติม buffer ด้วย memset ตอนเริ่มต้น page table entry จะถูกสร้างไว้ล่วงหน้า จึงหลีกเลี่ยง slow path ของ get_user_pages_fast ได้
    • ถ้าไม่ทำ memset ในตัวอย่าง throughput ลดลงเป็น 25.0GiB/s
    • เมื่อใช้ huge page ปรากฏการณ์นี้ไม่ได้เกิดขึ้นเหมือนกัน

ลดต้นทุนการจัดการหน้าด้วย huge page

  • x86-64 รองรับ 2MiB และ 1GiB huge page นอกเหนือจากหน้าปกติ 4KiB
    • การทดลองต่อจากนี้ครอบคลุมเฉพาะ 2MiB huge page
    • 1GiB page ค่อนข้างหายากและถือว่าเกินจำเป็นสำหรับงานนี้
  • huge page ลดต้นทุนการจัดการด้วยการแทนช่วงหน่วยความจำเดียวกันด้วยจำนวนหน้าที่น้อยกว่า
    • เมื่อแปลง virtual address เป็น physical address ขั้นของ page table ก็ลดลงหนึ่งขั้นด้วย
    • อาจช่วยลดภาระ TLB ของ CPU ได้ด้วย
  • คอขวดโดยตรงของการทดลองนี้ไม่ใช่ hardware page table walk แต่เป็น path ฝั่ง software ของ kernel อย่าง get_user_pages_fast
  • บน Linux สามารถขอใช้ huge page ได้โดยจัดสรรหน่วยความจำที่จัดแนวกับ 2MiB แล้วเรียก madvise(..., MADV_HUGEPAGE)
  • เมื่อนำ huge page มาใช้ throughput เพิ่มเป็น 51.0GiB/s
  • เหตุผลของ performance ที่ดีขึ้นไม่ใช่เพราะ struct page ชี้ไปยังหน้า 2MiB หนึ่งหน้าโดยตรง
    • โค้ด kernel โดยทั่วไปสมมติว่า struct page ชี้ไปยังขนาดหน้ามาตรฐานของ architecture ปัจจุบัน
    • huge page ถูกแทนด้วย head struct page และ tail struct page หลายตัว
    • 2MiB huge page อาจถูกแทนด้วย struct page ได้สูงสุด 512 ตัวเมื่อคิดตาม 4KiB
  • แต่หลังจากหา entry แรกได้แล้ว สามารถสร้าง struct page ตัวถัด ๆ ไปด้วย loop ง่าย ๆ ได้ จึงลดต้นทุนการไล่ page table หลายครั้ง
  • Kernel ตั้งแต่ Linux 5.17 เป็นต้นมามี struct folio ที่ระบุ head page อย่างชัดเจน ช่วยลดความจำเป็นในการตรวจ head/tail ตอน runtime

ลดต้นทุนการ sync ด้วย busy loop

  • หลังใช้ huge page แล้ว ใน perf เวลาใน wait_for_space และ __wake_up_common_lock เด่นชัดขึ้น
    • ต้นทุนการรอพื้นที่ที่เขียนได้
    • ต้นทุนการปลุกฝั่งอ่าน
  • หากต้องการเลี่ยงต้นทุนการ sync นี้ สามารถใช้ SPLICE_F_NONBLOCK กับ vmsplice และ splice ได้
    • ถ้าเขียนเข้า pipe ไม่ได้ จะ return EAGAIN ทันที
    • caller จะวน busy loop จนกว่าจะพร้อม
  • เมื่อใช้ busy loop throughput เพิ่มขึ้นถึง 62.5GiB/s
  • ต้นทุนก็ชัดเจนเช่นกัน
    • ระหว่างรอให้ vmsplice หรือ splice พร้อม จะยึดครอง CPU core เต็ม ๆ
    • เป็นวิธีแลกการใช้ CPU มากขึ้นเพื่อให้ได้ latency หรือ throughput
  • สุดท้าย synthetic benchmark นี้ถูกปรับปรุงจากประมาณ 3.5GiB/s เป็นประมาณ 65GiB/s

รายละเอียดที่เหลือและประเด็นเชิงปฏิบัติ

  • กระบวนการ optimize ดำเนินไปโดยดู output ของ perf ควบคู่กับ source code ของ Linux
  • หัวข้อที่กล่าวถึงเชื่อมโยงกับประเด็น high-performance programming ที่กว้างกว่า pipe และ splicing เอง
    • การทำงานแบบไม่คัดลอก

      • ring buffer
      • paging และ virtual memory
      • synchronization overhead
      • ในโค้ดจริง มีการจัดสรร buffer สองตัวแยกกันเพื่อลดการแข่งขันบน page table
      • get_user_pages เพิ่ม reference count ของ page table entry และ put_page ลดลง
      • ถ้า buffer สองตัวใช้ page table entry คนละตัว การแข่งขันในการแก้ reference count จะลดลง
      • การทดสอบรันโดยใช้ taskset pin process ./write และ ./read ไว้กับสอง core
      • repository ยังมี synthetic benchmark สำหรับ get_user_pages_fast ด้วย
      • สามารถวัดความต่างของความเร็วตามการใช้หรือไม่ใช้ huge page ได้
      • splicing ยังคงเป็นแนวคิดที่คลุมเครือและอันตราย และ issue ที่เกี่ยวข้องยังเป็นภาระต่อเนื่องสำหรับ kernel developer

1 ความคิดเห็น

 
GN⁺ 2023-10-06
ความคิดเห็นบน Hacker News
  • ถ้าผมเข้าใจถูก vmsplice เมื่อใช้ทั้งฝั่งอ่านและฝั่งเขียนพร้อมกัน จะดูใกล้เคียงกับ กลไก shared memory ขนาดเล็ก ระหว่างสองโปรเซส
    กล่าวคือทั้งสองโปรเซสต้องระมัดระวังอย่างยิ่งว่าเมื่อไรจะอ่านและเขียนบัฟเฟอร์ และหลังใช้แล้วจะคืนอย่างไร เป็นวิธีที่เร็วแต่ก็น่ากลัวในเวลาเดียวกัน และน่าเสียดายที่ implementation แบบซื่อ ๆ ที่ใคร ๆ ก็น่าจะเขียนนั้น ช้ากว่า ประสิทธิภาพที่เป็นไปได้ 20 เท่า

    • ถ้าคุณพยายามเขียนเวอร์ชันที่เร็วกว่า 20 เท่า เพื่อนร่วมงานจะมองว่าคุณทำให้มันซับซ้อนเกินไปและไม่เหมือนคนที่ทำงานเป็นทีม
    • ผมไม่คิดว่า vmsplice เป็นกลไก shared memory ขนาดเล็กระหว่างสองโปรเซส มันรองรับแค่ zero-copy จากหน่วยความจำของผู้ใช้ไปยัง pipe ส่วนทิศทางตรงกันข้ามยังเกิดการ copy
      ดูรายละเอียดที่ https://mazzo.li/posts/fast-pipes.html#fn10
  • สงสัยว่ามี ไลบรารีประมวลผลข้อมูล ที่ abstract pipe, socket, file และ memory พร้อมทำ optimization แบบนี้ให้ด้วยหรือไม่
    อยากรู้ว่ามีไลบรารีแบบนั้นใน C, C++, Rust หรือภาษา systems อื่น ๆ ไหม เพราะไม่คุ้นกับ API อย่าง splice() และ vmsplice() ที่กล่าวถึงในบทความ เลยสงสัยว่าเวลาสร้างแอปพลิเคชันระดับล่าง จะมีไลบรารีที่ใช้ optimization เหล่านี้ให้อัตโนมัติเมื่อทำได้หรือเปล่า ยังสงสัยด้วยว่า libuv, tokio, Netty จัดการสิ่งนี้ให้อัตโนมัติบน Linux หรือไม่ จากที่ค้นคร่าว ๆ ดูเหมือนว่าอาจจะเป็นไปได้

    • อาจต่างจาก flow ทั่วไป แต่สิ่งนี้ ไม่ portable เลยไม่ค่อยคุ้มที่จะทำเป็น abstraction มีแนวโน้มสูงว่าจะต้อง implement เองตรงจุดที่จำเป็น
      โค้ดระดับสูงใช้ความสามารถแบบนี้ค่อนข้างน้อย เพราะมันมีวัตถุประสงค์ค่อนข้างเฉพาะ และต้องทำให้เฉพาะทางกับ Linux ถ้าแค่ย้ายข้อมูลบน Linux โดยไม่ต้องมองเข้าไปในข้อมูล splice ก็มีประโยชน์ แอปอย่าง TCP/UDP proxy ต้องการมันแน่นอน แต่ไม่ค่อยเหมาะกับ HTTP server ธรรมดา ถ้ากำลังสร้างแอปแบบนี้ คุณจะเจอคีย์เวิร์ดอย่าง zero copy บ่อย ๆ และ splice จะเป็นหนึ่งในผลลัพธ์แรก ๆ ที่เห็น
    • มี crate สำหรับ tokio อยู่ ไม่ใช่อัตโนมัติ แต่อาจน่าสนใจ: https://lib.rs/crates/tokio-splice
    • น่าลองดู Cosh ตอนนี้ผมกำลังอ่าน paper นั้นและคิดตามอยู่ มันเป็นโมเดลที่ให้ abstraction การส่งข้อความ ขณะยังเปิดให้ทำ optimization ได้
      ดูเหมือนจะไม่ค่อยเป็นที่รู้จักนอกวงการวิจัย และการเขียน implementation ของ Cosh ที่มีประสิทธิภาพน่าจะใช้เวลาพอสมควร สรุปคือโหมดการส่งมีสามแบบคือ move, share, copy ตัวอย่างเช่น การส่งแบบ move คือการส่งมอบข้อมูลที่ผู้ส่งมีสิทธิ์อ่าน/เขียนไปให้ผู้รับโดยสมบูรณ์ และอาจ implement ได้ด้วยการ remap หน่วยความจำเสมือนใน page table นอกจากนี้ยังมีคุณสมบัติ strong/weak ที่ระบุว่าผู้ส่งและผู้รับเชื่อใจได้ว่าร่วมมือกันหรือไม่ หรือต้องแยกจากกันอย่างเข้มงวดด้วยการ remap สิทธิ์หน่วยความจำเสมือน พูดตรง ๆ ผมไม่แน่ใจว่ามันจะ optimize ได้ดีพอจนเทียบกับสิ่งอย่าง pipe ที่ optimize สุดขีดได้อย่างน่าเชื่อถือหรือไม่ และอาจกลายเป็นปัญหาแบบ “คอมไพเลอร์ที่ฉลาดพอ” ก็ได้ แต่ก็คิดว่าคุ้มค่าที่จะลอง
      [1] https://barrelfish.org/publications/trios14-baumann-cosh.pdf
  • การอภิปรายก่อนปี 2022: https://news.ycombinator.com/item?id=31592934

    • เมื่อขยายดู จะเป็นการอภิปรายเรื่อง “How fast are Linux pipes anyway?” และมีคอมเมนต์ราว 200 รายการในเดือนมิถุนายน 2022: https://news.ycombinator.com/item?id=31592934
  • ข้อเท็จจริงที่น่าประหลาดใจซึ่งบังเอิญได้รู้เมื่อ 4 ปีก่อนคือ การใช้ pipe ของ Linux อาจทำให้เกิด พฤติกรรมแบบไม่กำหนดแน่นอน ได้
    https://www.gibney.org/the_output_of_linux_pipes_can_be_inde...

    • ไม่ใช่เรื่องน่าแปลกอะไร pipe ที่สร้างขึ้นไม่ได้ส่งต่อข้อมูลที่ echo ออกมาจริง ๆ เลย
      (echo red; echo green 1>&2) | echo blue สร้าง subshell สองตัวโดยมีสัญลักษณ์ | คั่นกลาง subshell เป็นโปรเซสลูกของ shell ปัจจุบัน จึงสืบทอดคุณสมบัติสำคัญอย่างตาราง file descriptor ที่เปิดอยู่ subshell ทั้งสองทำงานพร้อมกัน และ shell แม่ก็แค่ wait() รอให้โปรเซสลูกทั้งหมดจบเท่านั้น โดยทั่วไปคาดเดาไม่ได้ว่าโปรเซสลูกตัวไหนจะทำงานก่อน และบนระบบ multicore ก็อาจทำงานพร้อมกันจริง ๆ ได้ standard output ของ subshell ฝั่งซ้ายเชื่อมกับปลายเขียนของ pipe ส่วน standard input ของ subshell ฝั่งขวาเชื่อมกับปลายอ่าน แต่ echo blue ไม่อ่าน input และทำแค่ output ดังนั้นจึงไม่มีอะไรถูกอ่านจาก pipe เลย echo green >&2 ส่ง standard output ไปยังปลายทางที่ standard error ชี้อยู่ ไม่ใช่ pipe สุดท้าย echo green กับ echo blue จึงเขียนไปยังไฟล์เดียวกัน ซึ่งน่าจะเป็น terminal โดยตรง ทำให้เกิด race condition และลำดับจะต่างกันตามว่าตัวไหนถูก schedule ก่อน
    • ถ้าคิดลึกขึ้นอีกนิด ก็เป็นเรื่องธรรมชาติอย่างยิ่ง โปรแกรมใน pipeline ทำงานพร้อมกัน
      ถ้าไม่เป็นแบบนั้น pipeline ก็ไม่มีประโยชน์ เช่น ใน pipeline ที่ดาวน์โหลดไฟล์ tar ด้วย curl แล้วแตกไฟล์ทันที หากต้องรอให้ curl จบก่อนแล้วค่อยรัน tar ก็จะเกิดปัญหาอย่างต้องเอาไฟล์ tar ชั่วคราวขนาดใหญ่ไปเก็บไว้ที่ไหน tar ต้องทำงานไปพร้อมกับตอนที่ curl กำลังรันอยู่ เพื่อให้ buffer เล็กและทำงานได้เร็ว control flow เพียงอย่างเดียวระหว่างโปรแกรมใน pipeline คือผ่าน standard input และ standard output ในตัวอย่างนี้เขียนไปที่ standard error ดังนั้นแน่นอนว่าไม่ได้อยู่ใน control flow ที่กำหนดแน่นอน
    • ถ้าชอบ zero-copy I/O ที่รวดเร็วบน Linux บทความนี้ก็น่าอ่านเช่นกัน
      เสริมอีกนิด เพื่อหลีกเลี่ยงความสับสน “Indeterministic” เป็นศัพท์ทางปรัชญา ส่วนศัพท์วิทยาการคอมพิวเตอร์คือ “nondeterministic”
      0. https://blog.superpat.com/zero-copy-in-linux-with-sendfile-a...
    • เรื่องนั้นน่าแปลกขนาดนั้นเลยหรือ? ถ้ารู้ว่าคาดว่าจะได้ output แบบไหน และทำไมถึงคิดอย่างนั้น ก็น่าจะช่วยคลายความสับสนได้ง่ายขึ้น
      คำสั่งนี้น่าจะถูกทำให้ดูประหลาดโดยตั้งใจ และถ้าเป็น code reviewer ก็คงต้องเอียงคอสงสัยแน่ ๆ มี echo red แต่ไม่ได้ถูกส่งต่อไปที่ไหนเลย อาจเป็นมุก “red herring” ก็ได้ echo green ไปที่ standard error จึงจะเห็นได้ก็ต่อเมื่อมันจบก่อน echo blue เท่านั้น ลำดับที่แน่นอนขึ้นกับ output buffering ซึ่งขึ้นกับว่า time slice ไหนถูกจัดสรรก่อน และเปลี่ยนไปตามจำนวน CPU กับโหลด ดังนั้นจึงไม่กำหนดแน่นอน แต่เป็นแบบเดียวกับที่ top ไม่กำหนดแน่นอน
    • มีกรณีที่เรื่องแบบนี้ก่อปัญหาจริง ๆ ไหม? พูดตรง ๆ ตัวอย่างนี้ดูค่อนข้างปรุงแต่งขึ้นมา
  • สรุปคือ หากสมมติว่าโปรแกรมทั้งสองถูกเขียนมาให้เหมาะสมที่สุดเท่าที่เป็นไปได้ ความเร็วสูงสุดของ pipe จะใกล้เคียงกับความเร็วที่คอร์หนึ่งของระบบสามารถอ่านและเขียนได้
    โดยพื้นฐานแล้ว kernel จะ map หน้า physical memory เดียวกันจาก standard output ของโปรแกรมหนึ่งไปยัง standard input ของอีกโปรแกรมหนึ่ง ดังนั้นงานจึงเป็น zero-copy หรือในสถานการณ์ที่เหมาะสมน้อยกว่า ก็ใกล้เคียงกับการ copy ครั้งเดียวที่รวดเร็ว เมื่อรู้ข้อเท็จจริงนี้แล้ว การทำงานประสิทธิภาพสูงมากด้วย shell script ที่เชื่อมเครื่องมือสองตัวขึ้นไปด้วย pipe ก็ทั้งคุ้มค่าและชวนขำ มันเป็นหนึ่งในเครื่องมือที่มีประโยชน์ที่สุดในกล่องเครื่องมือ

    • pipe จะเป็น zero-copy ก็ต่อเมื่อใช้ splice หรือ vmsplice เท่านั้น system call เฉพาะ Linux เหล่านี้ใช้งานยาก โดยเฉพาะ vmsplice
      โปรแกรมส่วนใหญ่และ shell filter ส่วนใหญ่ไม่ได้ใช้สิ่งนี้ ยกเว้นตัวอย่างเด่น ๆ อย่าง pv จึงต้องจ่ายต้นทุนจากการ copy เข้า kernel memory แล้ว copy ออกมาอีกครั้ง
    • เข้าใจว่าข้อจำกัดร้ายแรงของ pipe คือ บน x86 Linux สามารถ buffer ได้แค่ 64KB / 16 หน้า เท่านั้น โดยทั่วไปจึงมีแนวโน้มจะช้ากว่า bandwidth ระหว่างคอร์กับหน่วยความจำ
    • ดังนั้นมันไม่ได้สำคัญอย่างที่โปรแกรมเมอร์ที่ใช้ thread จำนวนมากคิด
      แอปพลิเคชันที่กำลังสร้างอยู่อาจถูกทำให้สะอาดกว่าได้ด้วย pipe+process หรือ green/user-space thread ขึ้นกับลักษณะโหลด อาจสะดวกน้อยกว่า แต่ message passing มักดีกว่า นรกของ deadlock
    • ที่ตลกคือผู้คนหรือทีมใช้เวลาหลายสัปดาห์และเงินจำนวนมากเพื่อให้ได้ผลลัพธ์ที่แย่กว่า
    • ไม่ค่อยรู้เรื่องเวทมนตร์ระดับระบบแบบนี้นัก แต่ข้อมูลต้องขึ้นไปถึง memory ทั้งหมดจริง ๆ ไหม? หรือ cache ช่วยกันการวิ่งไปกลับนั้นไว้?
  • บทความนี้พูดถึงวิธีทำให้ pipe ของ Linux เร็วขึ้น แต่แนวทางอื่นอย่าง shared memory หรือ message queue ก็ยังอาจเร็วกว่าได้
    ในระบบที่ต้องย้ายข้อมูลจำนวนมากอย่างรวดเร็ว ขั้นตอนเพิ่มเติมของ pipe อาจทำให้ช้าลงได้ แม้เวลาหลาย thread แชร์ข้อมูลกัน pipe ก็อาจสร้างปัญหามากกว่าวิธีอื่น ดังนั้นการปรับปรุงในบทความอาจไม่ได้ช่วยมากนักในสถานการณ์จริงที่ความเร็วสำคัญ

    • ยกตัวอย่างได้ไหม? เมื่อประมวลผลข้อมูลเป็นก้อน ๆ การเลือกสิ่งอย่าง io_uring ก็มีข้อดี
      แต่ในการสื่อสารสองทาง ไม่ว่าฝั่งไหนก็ต้องมีการแจ้งเตือนว่าข้อมูลพร้อมแล้ว อาจไม่อยากเผา CPU ด้วยการ polling และก็ไม่แน่ใจว่าทางเลือกเหล่านั้นจัดการ synchronization นั้นได้เร็วกว่า pipe อย่างไร
    • การใช้ไลบรารี message queue ยังมีข้อดีคือไม่ต้องกังวลกับ ความไม่เข้ากันระหว่างหลายแพลตฟอร์ม มากนัก
  • รู้อยู่แล้วเรื่องอย่าง page table แต่พอเชื่อมโยงเข้ากับ การวิเคราะห์ประสิทธิภาพ ผ่าน perf ก็เห็นชัดขึ้นว่ามันเป็นแกนกลางของ throughput แค่ไหน

  • pipe ยอดเยี่ยมมาก จริง ๆ แล้วไม่ค่อยสำคัญเท่าไรว่าโปรเซสอื่นจะอยู่บน CPU อื่นหรืออยู่บนเครื่องอื่น
    https://github.com/nathants/s4/blob/master/examples/nyc_taxi...

  • pipe เร็วพอสำหรับการประกอบ cat, sed, awk, cut, grep, uniq, jq ฯลฯ ซ้ำ ๆ