2 คะแนน โดย GN⁺ 2025-07-19 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • lsr เป็นโปรแกรมทดแทน ls(1) แบบใหม่ที่พัฒนาด้วยไลบรารี IO ชื่อ ourio ซึ่งสร้างบน io_uring
  • เมื่อเทียบกับ ls แบบดั้งเดิมและเครื่องมือทางเลือก (eza, lsd, uutils ls) พบว่า รันคำสั่งได้เร็วมาก และ ใช้ system call น้อยกว่ามากกว่า 10 เท่า
  • IO หลักทั้งหมด เช่น การเปิดไดเรกทอรี, stat, lstat ถูกจัดการแบบ อะซิงโครนัสและแบบแบตช์ด้วย io_uring เพื่อรีดประสิทธิภาพสูงสุด ยิ่งมีไฟล์มากยิ่งเร็ว
  • ใช้ StackFallbackAllocator ของ Zig เพื่อลดการเรียก mmap ระหว่างการจัดสรรหน่วยความจำให้น้อยที่สุด
  • คอมไพล์แบบสแตติกโดยไม่ใช้ dynamic linking ทำให้ ขนาดไฟล์ไบนารีเล็กกว่า ls แบบเดิมอีกด้วย

บทนำและความสำคัญ

  • โปรเจกต์ lsr เป็น เครื่องมือแสดงรายการไดเรกทอรีความเร็วสูง ที่ใช้ io_uring ในฐานะ ตัวแทนของคำสั่ง ls ทั่วไป
  • เมื่อเทียบกับ ls, eza, lsd, uutils ls เดิม ๆ แล้ว ให้ประสิทธิภาพโดดเด่นทั้งในด้าน ความเร็วในการรัน และ ปริมาณการใช้ system call
  • ใช้ไลบรารี IO ที่พัฒนาขึ้นเอง (ourio) เพื่อจัดการ IO ให้ได้มากที่สุดด้วยตัวเอง
  • ผลเบนช์มาร์กยืนยันว่า lsr ให้ทั้งประสิทธิภาพและคุณภาพที่ดีแม้ในสภาพแวดล้อมที่มีไฟล์จำนวนมาก

ผลการเบนช์มาร์ก

  • ใช้ hyperfine วัดเวลาการรันของแต่ละคำสั่งในไดเรกทอรีที่มีไฟล์ทั่วไปจำนวน n ไฟล์
    • lsr -al ที่ช่วง 10–10,000 ไฟล์ ทำเวลาได้ สั้นกว่ามาก เมื่อเทียบกับ ls และทางเลือกอื่น
    • ตัวอย่าง: ที่ 10,000 ไฟล์ lsr ทำได้ 22.1ms เร็วกว่า ls เดิม (38.0ms), eza (40.2ms), lsd (153.4ms), uutils ls (89.6ms) อย่างชัดเจน
  • การนับ system call ใช้ strace -c
    • lsr -al: ใช้เพียง 20 ครั้งเป็นอย่างน้อย (n=10) และสูงสุดแค่ 848 ครั้ง (n=10,000) ซึ่งต่ำมาก
    • ls ใช้สูงสุด 30,396 ครั้ง (n=10,000), lsd สูงถึง 100,512 ครั้ง และตัวเลือกอื่นก็อยู่ในระดับหลายพันถึงหลายแสนครั้ง
    • ภายใต้เงื่อนไขเดียวกัน lsr ใช้ syscall น้อยกว่าอย่างน้อย 10 เท่า และให้ประสิทธิภาพสูงสุด

โครงสร้างและแนวทางการทำงานของ lsr

  • โปรแกรมทำงานเป็น 3 ขั้นตอน ได้แก่ การพาร์สอาร์กิวเมนต์, การรวบรวมข้อมูล, การแสดงผลข้อมูล
  • IO ทั้งหมดเกิดขึ้นในขั้นตอนที่สองคือ การรวบรวมข้อมูล โดย พยายามจัดการการเข้าถึงไฟล์และการดึงข้อมูลทั้งหมดผ่าน io_uring ให้มากที่สุดเท่าที่เป็นไปได้
    • ทั้งการเปิดไดเรกทอรีเป้าหมาย, stat, lstat, การอ่านข้อมูลเวลา/ผู้ใช้/กลุ่ม ล้วนทำบนพื้นฐานของ io_uring
    • การประมวลผล stat แบบแบตช์ช่วยลดจำนวน system call ได้อย่างมาก
  • ใช้ Zig StackFallbackAllocator เพื่อจองหน่วยความจำล่วงหน้า 1MB และลด system call เพิ่มเติมอย่าง mmap ให้น้อยที่สุด

การคอมไพล์แบบสแตติกและการปรับแต่งประสิทธิภาพ

  • เนื่องจากเป็น การคอมไพล์แบบสแตติกเต็มรูปแบบโดยไม่ลิงก์กับ libc แบบไดนามิก จึงมีโอเวอร์เฮดขณะรันต่ำมาก
  • เมื่อเทียบกับ GNU ls ขนาดบิลด์แบบ ReleaseSmall ของ lsr เล็กกว่า คือ 138.7KB เทียบกับ 79.3KB
  • อย่างไรก็ตาม lsr ยังไม่รองรับ locale (ภาษา/ภูมิภาค) ขณะที่ ls ทั่วไปมีโอเวอร์เฮดจากการรองรับหลายภาษา

การวิเคราะห์ system call และประเด็นด้านประสิทธิภาพ

  • lsd เรียก clock_gettime มากกว่า 5 ครั้งต่อไฟล์ โดยยังไม่ทราบสาเหตุแน่ชัด (คาดว่าอาจใช้วัดเวลาในภายในโปรแกรม)
  • การเรียงลำดับ (sorting) กินเวลาส่วนสำคัญของงานทั้งหมด (ประมาณ 30%)
    • uutils ls มีประสิทธิภาพด้าน system call ที่ดี แต่ช้าลงในขั้นตอนการเรียงลำดับ
  • เพียงแค่นำ io_uring มาใช้ ก็แสดงให้เห็นถึงศักยภาพของ การเพิ่มประสิทธิภาพแบบก้าวกระโดด ในสภาพแวดล้อมที่มี IO หนัก เช่น เซิร์ฟเวอร์

บทสรุป

  • ใช้เวลาในการพัฒนาไม่นาน แต่ผลของการปรับแต่ง syscall ดีเกินคาด
  • lsr เป็นตัวแทน ls เชิงทดลองที่ทำได้พร้อมกันทั้ง ความเร็วสูง, system call ต่ำ, และขนาดกะทัดรัด
  • เหมาะมากกับสภาพแวดล้อมที่มีไฟล์จำนวนมาก หรือระบบที่ความสำคัญอยู่ที่ประสิทธิภาพ IO สูง
  • แม้จะยังมีข้อจำกัดบางอย่าง เช่น ไม่รองรับ locale แต่ทั้งในการใช้งานจริงและผลเบนช์มาร์กก็แสดงผลลัพธ์ที่น่าทึ่ง

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

 
GN⁺ 2025-07-19
ความคิดเห็นบน Hacker News
  • ผู้เขียนโครงการมาแสดงตัว และบอกว่าสามารถอ่านบทความแนะนำ lsr ที่สร้างบน io_uring ได้ที่นี่

    • แชร์ประสบการณ์ที่เคยทำโปรเจ็กต์ I18N ที่ Sun ว่า ถ้าจะรองรับหลายสภาพแวดล้อม (localization, utf8 ฯลฯ) ก็ต้องเพิ่มการประมวลผลหลายอย่างเข้าไปในโปรแกรม ทำให้ยิ่งมีต้นทุนในการสร้างผลลัพธ์มากขึ้นและความเร็วก็ยิ่งลดลง เดิมที UNIX ls(1) ออกแบบมาเรียบง่ายและเร็วมาก แต่เมื่อมีการเพิ่มฟีเจอร์ต่าง ๆ, virtual file system (VFS), ชุดอักขระหลากหลาย, การรองรับสี ฯลฯ ต้นทุนเล็ก ๆ ก็สะสมจนช้าลง มองว่านี่เป็นประเด็นถกเถียงที่น่าสนใจเรื่องต้นทุนของ abstraction ที่ io_uring จัดการอยู่
    • โปรเจ็กต์ bfs ก็ใช้ io_uring ด้วย (ลิงก์ซอร์สโค้ด) เลยอยากรู้การเปรียบเทียบประสิทธิภาพระหว่าง lsr กับ bfs -ls ตอนนี้ bfs ใช้ io_uring เฉพาะตอนทำงานหลายเธรด แต่ก็น่าคิดว่าจะใช้กับ single thread (bfs -j1) ด้วยหรือไม่
    • ถ้าใช้ tim (ลิงก์แนะนำ) วัดเวลา น่าจะดีกว่า hyperfine แม้จะเขียนด้วย Nim เลยอาจท้าทายอยู่บ้าง แต่ชื่อคล้ายกันแบบนี้ก็ตลกดีที่เป็นเรื่องบังเอิญ
    • กำลังคิดจะพอร์ตโปรเจ็กต์ C++ ไปเป็น Zig ส่วน libevring ที่ทำเองก็ยังอยู่ช่วงต้น จึงเปิดกว้างว่าจะเปลี่ยนไปใช้ ourio แทนถ้าจำเป็น มองว่าถ้าโปรเจ็กต์ฝั่ง Zig รองรับ C/C++ bindings ก็จะมีประโยชน์มากเวลาย้ายจาก C/C++ ไป Zig
    • บทความแนะนำชิ้นนั้นอธิบายพื้นหลังได้ดีกว่า จึงตั้งใจจะใช้เป็นลิงก์หลัก และเพิ่มเธรดเรโปไว้ด้านบน
  • สงสัยว่า lsr จะมีประสิทธิภาพแค่ไหนบนเซิร์ฟเวอร์ NFS โดยเฉพาะในสภาพเครือข่ายที่ไม่ดี การใช้ blocking POSIX syscall กับบริการเครือข่ายที่ไม่เสถียรเป็นจุดอ่อนที่ชัดเจนของการออกแบบ NFS อยู่แล้ว และก็น่าสังเกตว่า io_uring จะช่วยบรรเทาปัญหานี้ได้แค่ไหน

    • ผู้ออกแบบ NFS ทำให้ระบบกระจายทำงานได้สม่ำเสมอมากราวกับเป็นฮาร์ดไดรฟ์ ข้อดีคือเครื่องมือเดิม ๆ (เช่น ls) ไม่ต้องจัดการข้อผิดพลาดเครือข่ายด้วยตัวเอง เดิมโปรโตคอล NFS ไม่มีสถานะ ทำให้ไคลเอนต์กู้คืนอัตโนมัติได้แม้เซิร์ฟเวอร์รีบูต เลยอยากรู้ว่า io_uring จะส่งต่อ error ในเคสแบบนี้ได้ดีแค่ไหน และเวลามี NFS timeout จะจัดการอย่างไร
    • ที่บ้านใช้ NFS $HOME ร่วมกันในหลายเครื่องพีซี และถ้าเครือข่ายดีพร้อมหลีกเลี่ยงเคสยาก ๆ อย่างการเขียนแบบขนาน ประสบการณ์ใช้งาน NFS โดยรวมก็ค่อนข้างน่าพอใจ แต่ก็เคยลำบากตอนสายเครือข่ายมีปัญหาแล้วเกิดอาการหลุด
    • สถานการณ์ที่แอปซึ่งกำลังอ่านโฟลเดอร์บน NFS กด ctrl+c ไม่ได้ เป็นปัญหาที่รู้กันดี ตามทฤษฎี mount option intr เคยรองรับการส่งสัญญาณไปยัง operation ที่กำลังค้างอยู่บนเซิร์ฟเวอร์ระยะไกลเพื่อให้ยกเลิกได้ แต่บน Linux ถูกถอดออกไปนานแล้ว (ตอนนี้มีแค่ soft option) (อ้างอิง 1, อ้างอิง 2 (FreeBSD รองรับ))
    • Samba ก็มีปัญหาคล้ายกันนี้
  • น่าสนใจที่แม้จะลดจำนวนการเรียก syscall ลงได้ 35 เท่า แต่ความเร็วกลับดีขึ้นแค่ราว 2 เท่า

    • syscall ส่วนใหญ่เกิดผ่าน VDSO จึงไม่ได้มีต้นทุนสูงมาก
    • เคยอ่าน benchmark เกี่ยวกับ io_uring ที่ออกมาว่า syscall ที่ใช้ io_uring กลับหนักกว่า syscall แบบเดิมด้วยซ้ำ ถึงอย่างนั้นในเชิงความรู้สึกก็ยังถือว่าดีขึ้นมาก จำแหล่งที่มาแน่ชัดไม่ได้ แต่ติดใจอยู่พอสมควร
  • โปรเจ็กต์นี้น่าสนใจกว่าในฐานะตัวอย่างการใช้ io_uring เพื่อดูว่าระยะยาวจะได้ประโยชน์ด้านความเร็วจริงแค่ไหน หรือในฐานะตัวอย่างสอนใช้งาน มากกว่าจะรู้สึกว่าจำเป็นต้องมีเมื่อเทียบกับเครื่องมือเดิมอย่าง eza ถ้าลิสต์ไฟล์หนึ่งหมื่นไฟล์ใช้เวลา 40ms เทียบกับ 20ms ก็คงไม่รู้สึกถึงความต่างเลยในการใช้งานครั้งเดียว

    • เป็นโปรเจ็กต์ทดลองที่ทำขึ้นเพื่อความสนุกและเพื่อเรียนรู้วิธีใช้ io_uring เป็นหลัก เวลาที่ประหยัดได้จริงแทบไม่มีนัยสำคัญ (ระดับประหยัดได้รวม ๆ 5 วินาทีตลอดชีวิต) และนั่นก็ไม่ใช่ประเด็นสำคัญ
    • แต่ในไดเรกทอรีที่มีไฟล์ JSON หลายล้านไฟล์จริง ๆ การรัน ls/du อาจกินเวลาหลายนาที คำสั่งพื้นฐานของ coreutils มักใช้ประสิทธิภาพของ SSD สมัยใหม่ได้ไม่เต็มที่
  • lsr ก็ดี แต่เรื่องการลงสีและรองรับไอคอน eza ยังดีกว่า ตัวผู้แสดงความเห็นตั้งค่าเป็น eza --icons=always -1 เลยทำให้ไฟล์เพลง (.opus เป็นต้น) แสดงเป็นไอคอนและสีอัตโนมัติ ขณะที่ใน lsr มันดูเป็นแค่ไฟล์ธรรมดา ถึงอย่างนั้นก็รู้สึกชัดว่า lsr แพตช์ง่ายและเร็วมาก อีกทั้งยังหวังว่าจะมีการทำ cat และยูทิลิตีอื่น ๆ ในแนวเดียวกันด้วย รวมถึงรู้สึกว่าน่าสนใจที่ใช้ tangled.sh และ atproto และเพราะเขียนด้วย Zig เลยรู้สึกว่าเข้าถึงง่ายกว่า Rust สำหรับมือใหม่

    • bat คือโปรแกรมแทน cat แบบสมัยใหม่ (ไปที่ bat)
    • ถ้าจะรองรับการลงสี วิธีที่ดีที่สุดน่าจะเป็นการใช้มาตรฐานอย่าง LS_COLORS/dircolors GNU ls แสดงสีได้สวยดี
  • เคยสงสัยว่าทำไมเครื่องมือ CLI ทุกตัวไม่ใช้ io_uring ทั้งหมด ตัวผู้แสดงความเห็นบอกว่าตอนต่อ nvme ผ่าน usb 3.2 gen2 เครื่องมือทั่วไปได้ความเร็ว 740MB/s แต่เครื่องมือที่ใช้ aio หรือ io_uring ไปได้ถึง 1005MB/s และมองว่าน่าจะมีผลจากกลยุทธ์ความยาวคิวหรือการลด lock ด้วย

    • ที่ผ่านมามักเขียนให้พกพาได้โดยไม่ใช้การแยกเงื่อนไขด้วยแมโครอย่าง #ifdef มากนัก จึงทำให้การนำเทคโนโลยีใหม่ที่ผูกกับแพลตฟอร์มหรือเวอร์ชันมาใช้เป็นไปอย่างช้า ๆ ทุกวันนี้ก็รู้สึกว่าข้อดีของความเข้ากันได้ระหว่างแพลตฟอร์มแนว POSIX หลายแบบไม่ได้มากเหมือนแต่ก่อนแล้ว
    • การใช้ io_uring อย่างมีประสิทธิภาพต้องอาศัยโมเดล asynchronous แบบ event-driven ขณะที่เครื่องมือ CLI เดิมส่วนใหญ่เขียนแบบตรงไปตรงมาและลำดับขั้น ถ้าระดับภาษารองรับ async ได้เป็นธรรมชาติกว่านี้ การพอร์ตก็คงง่ายขึ้น แต่ตอนนี้มักต้องรีแฟกเตอร์ครั้งใหญ่ io_uring เองก็ยังไม่ถือว่านิ่งสมบูรณ์ จึงอาจต้องรอดูเทคโนโลยีใหม่ ๆ ต่อไป หรืออาจมีเครื่องมือพอร์ตอัตโนมัติ/AI เข้ามาช่วยในอนาคต
    • ช่วงแรกที่ io_uring ถูกนำมาใช้มีปัญหาด้านความปลอดภัยสำคัญอยู่มาก (ราว 2 ปีก่อน) แม้ตอนนี้จะแก้ไปได้เยอะแล้ว แต่ก็ส่งผลเสียต่อการยอมรับ
    • io_uring เป็นเรื่องที่ยากมากในมุมความปลอดภัย
    • io_uring ยังเป็นเทคโนโลยีใหม่มาก ขณะที่ coreutils (รวมถึงแพ็กเกจต้นน้ำก่อนหน้า) มีประวัติยาวนานหลายสิบปี กว่าที่ system call แบบ “shared ring buffer” จะกลายเป็นมาตรฐานแทนวิธี synchronous เดิมก็คงต้องใช้เวลา
  • มีคนสังเกตผ่าน strace ว่า lsd เรียก clock_gettime ราว 5 ครั้งต่อหนึ่งไฟล์ ไม่แน่ใจสาเหตุที่แท้จริง อาจเป็นการคำนวณว่าแต่ละ timestamp คือ “กี่นาที/ชั่วโมง/วันก่อน” หรืออาจเป็นมรดกจากไลบรารีเก่าก็ได้

    • ทุกวันนี้ clock_gettime ไม่ใช่ syscall จริงแล้ว แต่จัดการผ่าน vDSO (man 7 vDSO) เลยมีคนสงสัยว่า Zig อาจไม่ได้ใช้โครงสร้างนี้หรือเปล่า
  • อาจจะนอกประเด็นไปนิด แต่มีคนอยากรู้จากประสบการณ์จริงหรือค่า benchmark ว่าในสภาพแวดล้อม 10G NIC บนเซิร์ฟเวอร์ระดับ enterprise สูงอย่าง Mellanox 4 หรือ 5 นั้น io_uring ช่วยลด socket latency overhead ได้ระดับกี่ไมโครวินาทีเมื่อเทียบกับ LD_PRELOAD และดูเหมือนผลของทั้งสองอย่างจะไม่ทับซ้อนกัน จึงอยากได้ตัวเลขจากคนที่เคยลองจริง

  • io_uring ยังไม่รองรับ getdents ดังนั้นประโยชน์หลักจึงออกมาเด่นกับ bulk stat (เช่น ls -l) เลยรู้สึกเสียดายว่าถ้าทำ getdents ให้ asynchronous และประมวลผลซ้อนกันได้ก็น่าจะดี

    • ถ้า POSIX มาตรฐาน operation แบบ NFS readdirplus (getdents + stat) ขึ้นมาได้จริง ข้อได้เปรียบบางส่วนของ io_uring ก็น่าจะถูกหักล้างไป
  • รู้สึกขำดีที่มีไอคอนให้กับนามสกุล .mjs และ .cjs แต่กลับไม่มีสำหรับนามสกุลอย่าง .c, .h, .sh