1 คะแนน โดย GN⁺ 2025-05-23 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • พบว่า AV1 ดีโคเดอร์ rav1d ที่เขียนด้วย Rust ช้ากว่า dav1d ที่พัฒนาด้วย C อยู่ราว 9%
  • ยืนยันผลการเพิ่มความเร็วได้จากการปรับปรุง การ initialize บัฟเฟอร์ และการปรับปรุงลอจิกการเปรียบเทียบ struct โดยได้ผลดีขึ้นแยกกันที่ 1.5% และ 0.7%
  • ใช้เครื่องมือโปรไฟล์ samply เพื่อระบุสาเหตุของความต่างด้านประสิทธิภาพระหว่างทั้งสองเวอร์ชันอย่างชัดเจน
  • เพิ่มประสิทธิภาพใน Rust ด้วยการเปรียบเทียบแบบ byte-level แทนการใช้ implementation เริ่มต้นของ PartialEq
  • การปรับแต่งครั้งนี้ช่วยลดช่องว่างด้านประสิทธิภาพรวมได้ราว 30% แต่ยังมีพื้นที่ให้ปรับปรุงต่ออีก

พื้นหลังและแนวทาง

  • rav1d เป็นโปรเจ็กต์ที่พอร์ต dav1d AV1 ดีโคเดอร์มาเป็น Rust ด้วย c2rust พร้อมนำฟังก์ชัน asm ที่ปรับแต่งไว้และการปรับปรุงด้านความปลอดภัยตามแบบของภาษา Rust เข้ามาใช้
  • มีการกำหนดเกณฑ์ประสิทธิภาพพื้นฐานแบบเปิดเผยไว้แล้ว และในสถานะปัจจุบัน rav1d ที่พัฒนาด้วย Rust ช้ากว่า dav1d ที่พัฒนาด้วย C อยู่ราว 5%
  • การวิเคราะห์นี้โฟกัสที่ความต่างของ runtime ของไบนารีเมื่อใช้ input เดียวกัน แทนที่จะมองโครงสร้างโดยรวมของวิดีโอดีโคเดอร์ที่มีความซับซ้อน
  • เปรียบเทียบอย่างเป็นระบบด้วย เครื่องมือวัดประสิทธิภาพ (hyperfine) และ profiler (samply)
  • สภาพแวดล้อมที่ใช้ทดสอบคือ macOS ชิป M3 และลดความซับซ้อนด้วยการรันแบบเธรดเดียว

การวัดประสิทธิภาพ: เปรียบเทียบค่าพื้นฐาน

  • ทำการ build และ benchmark ด้วยไฟล์ทดสอบเดียวกัน (Chimera-AV1-8bit-1920x1080-6736kbps.ivf)
  • rav1d ใช้เวลาราว 73.9 วินาที ส่วน dav1d ใช้ราว 67.9 วินาที พบความต่างของเวลาในการรันประมาณ 6 วินาที (9%)
  • คอมไพเลอร์แต่ละตัว (Clang, Rustc) ใช้ LLVM เวอร์ชันแทบจะเหมือนกัน

การวิเคราะห์ด้วยโปรไฟล์

  • ใช้ profiler อย่าง samply เพื่อเปรียบเทียบจำนวน sample ระดับฟังก์ชันของแต่ละ executable
  • ตรวจดูเส้นทางการเรียกใช้และการกระจายของ sample ของฟังก์ชันแอสเซมบลีที่อิงกับ NEON (ARM SIMD) อย่างเจาะจง
  • dav1d แยกเป็นฟังก์ชัน filter ต่างหากเพื่อแตกแขนงไปเรียกฟังก์ชัน asm ขณะที่ rav1d จัดการทั้งหมดผ่านฟังก์ชัน dispatch เดียว
  • พบว่า cdef_filter_neon_erased มีจำนวน Self sample มากกว่าผลรวมของสองฟังก์ชันใน dav1d อยู่ราว 270 sample (คิดเป็นประมาณ 1% ของทั้งหมด)
  • จากการวิเคราะห์พบช่วงที่มีการ initialize บัฟเฟอร์ชั่วคราว (zero-initialized buffer) ขนาดใหญ่โดยไม่จำเป็น

การปรับแต่งเพื่อตัดการ initialize บัฟเฟอร์

  • Rust จะทำ zeroing ให้อัตโนมัติเพื่อความปลอดภัยด้วยรูปแบบอย่าง [0u16; LEN]
  • แต่ C (dav1d) ไม่ได้ทำ zeroing ให้บัฟเฟอร์แบบชัดเจน และจะเขียนค่าเฉพาะช่วงที่ใช้งานจริงเท่านั้น
  • ใน Rust จึงใช้ std::mem::MaybeUninit เพื่อตัดค่าใช้จ่ายจากการ initialize ที่ไม่จำเป็น
  • ทำให้จำนวน Self sample ของฟังก์ชัน cdef_filter_neon_erased ลดลงมากจาก 670 เหลือ 274
  • นอกจากนี้ยังย้ายการ initialize ของบัฟเฟอร์ Align16 ขนาดใหญ่อีกตัวออกไปไว้นอกลูป (hoist) เพื่อลดต้นทุนการ initialize ให้เหลือเพียง 1 ครั้ง
  • หลังปรับแต่งแล้ว benchmark อยู่ที่ราว 72.6 วินาที ดีขึ้น 1.2 วินาที (1.5%)

การปรับแต่งการเปรียบเทียบ struct

  • จากการวิเคราะห์ inverted stack ในโปรไฟล์ พบว่าฟังก์ชัน add_temporal_candidate ทำงานได้ไม่มีประสิทธิภาพเกินคาด
  • การเปรียบเทียบฟิลด์ของ struct Mv ภายในฟังก์ชันนี้ (จากการ derive PartialEq อัตโนมัติ) สร้างโค้ดที่ช้ากว่าที่ควรโดยไม่จำเป็น
  • ฝั่ง C ใช้ union เพื่อเปรียบเทียบอย่างมีประสิทธิภาพในระดับ uint32_t
  • ฝั่ง Rust หลีกเลี่ยง unsafe โดยใช้ trait zerocopy::AsBytes เพื่อทำการเปรียบเทียบในระดับ byte slice
  • การปรับแต่งนี้เพิ่มประสิทธิภาพได้อีก 0.5 วินาที (ประมาณ 0.7%)

ผลลัพธ์และสรุป

  • การปรับแต่งง่าย ๆ สองจุด (ตัดการ initialize บัฟเฟอร์, เปรียบเทียบ struct แบบไบต์) ช่วยลด runtime ได้รวมกว่า 2%
  • ยังเหลือช่องว่างด้านประสิทธิภาพอีกราว 6% และยังมีโอกาสปรับแต่งเพิ่มเติมได้มาก
  • ยืนยันได้ว่าวิธีเปรียบเทียบ snapshot จาก profiler มีประสิทธิภาพ
  • มีความเป็นไปได้สูงที่จะทำ optimization เพิ่มเติมบน rav1d และ dav1d จากการวิเคราะห์ snapshot
  • ด้วย feedback และความร่วมมืออย่างแข็งขันจากผู้ดูแลโปรเจ็กต์ จึงสามารถปรับปรุงได้โดยไม่กระทบความปลอดภัย

สรุปย่อ

  • ใช้เครื่องมือ profiler (samply) และ benchmark (hyperfine) วิเคราะห์ความต่างของ runtime 6 วินาที (9%) ระหว่าง rav1d กับ dav1d อย่างละเอียด
  • การปรับแต่งหลักมี 2 อย่าง:
    • ตัดการ zeroing บัฟเฟอร์ที่ไม่จำเป็นในโค้ดเฉพาะทาง ARM (1.2 วินาที, -1.6%)
    • เปลี่ยน implementation ของ PartialEq สำหรับ struct ตัวเลขขนาดเล็กให้เป็นการเปรียบเทียบไบต์ที่เร็วกว่า (0.5 วินาที, -0.7%)
  • แต่ละการปรับแต่งกระชับ ใช้โค้ดเพียงไม่กี่สิบบรรทัด และไม่ต้องเพิ่ม unsafe ใหม่
  • บรรลุทั้งความน่าเชื่อถือและคุณภาพที่ดีขึ้นผ่านความร่วมมือกับผู้ดูแลโปรเจ็กต์และการรีวิว PR
  • ยังเหลือช่องว่างด้านประสิทธิภาพอีกราว 6% จึงมีพื้นที่มากพอสำหรับการศึกษา optimization เพิ่มเติมด้วยการเปรียบเทียบแบบอิง profiler

ลองนำแนวทางนี้ไปใช้ดูได้เลย! บางที rav1d อาจเร็วกว่า dav1d ได้ในที่สุด 👀🦀

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

 
GN⁺ 2025-05-23
ความคิดเห็นจาก Hacker News
  • มีการแชร์ความเห็นว่าประเด็นการเปรียบเทียบ u16 สองค่าน่าสนใจ พร้อมแนบลิงก์อีชชูที่เกี่ยวข้อง https://github.com/rust-lang/rust/issues/140167
    • แสดงความแปลกใจที่ไม่มีการพูดถึง store forwarding ในการถกเถียง ผลการสร้างโค้ดที่ -O3 ดูเกินไปหน่อย แต่ที่ -O2 ถือว่าสมเหตุสมผล พร้อมอธิบายอย่างเจาะจงว่าถ้า struct ตัวใดตัวหนึ่งเพิ่งผ่านการคำนวณมา การพยายามโหลด 32 บิตอาจทำให้ store forwarding ล้มเหลวและทำให้การเพิ่มประสิทธิภาพไร้ความหมาย อีกทั้งในกรณีที่ไม่ inline/ไม่ใช้ PGO คอมไพเลอร์ก็ขาดข้อมูลที่จำเป็นต่อการตัดสินใจว่าเหมาะจะ optimize หรือไม่
    • รู้สึกดีที่การคุยในอีชชูไม่ได้เต็มไปด้วยคอมเมนต์แนว “ฉันก็เจอเหมือนกัน” หรือ “เมื่อไหร่จะซ่อม” และแชร์ความเห็นตรงไปตรงมาว่าในฐานะเว็บดีเวลอปเปอร์ GitHub Issues นั้นไม่น่าพอใจ
    • มองว่ากรณีนี้เป็นตัวอย่างที่แสดงให้เห็นว่าการพัฒนาคอมไพเลอร์ซับซ้อนแค่ไหน และมั่นใจว่าคอมไพเลอร์สาย C เองก็คงจัดการปัญหาแบบนี้ได้ไม่ดีกว่าเท่าไร
  • สงสัยว่าในบล็อกโพสต์นั้นแทรกผลลัพธ์จาก profiler อย่างไร ถามว่าเป็นการคัดลอก HTML node มาตรงๆ หรือไม่
  • รู้สึกว่าน่าสนใจที่บทความเรื่องประโยชน์ด้านประสิทธิภาพจากการละเว้นการล้างค่าเริ่มต้นบัฟเฟอร์ (zeroing) ออกมาต่อจากบทความที่เกี่ยวข้องเมื่อไม่กี่วันก่อน พร้อมแชร์ลิงก์บทความก่อนหน้า https://news.ycombinator.com/item?id=44032680
  • ชี้ว่าชื่อบทความหลักดูถ่อมตัวเกินไปเมื่อเทียบกับผลลัพธ์จริง ทั้งที่ความจริงคือได้ความเร็วเพิ่ม 2.3% จาก optimization ดีๆ สองอย่าง
    • มีความเห็นว่าการปรับปรุง 1.5% นั้นมีผลเฉพาะกับ aarch64 ดังนั้นการพูดถึงเป็นตัวเลขรวมอาจไม่ค่อยยุติธรรมนัก ถ้าคิดสัดส่วน arm/x86 ก็ควรนับราวครึ่งหนึ่ง
  • ประเมินว่าโพสต์นี้มีประโยชน์ และการค้นพบโค้ดที่ไม่มีประสิทธิภาพในการเปรียบเทียบคู่จำนวนเต็ม 16 บิตก็น่าประทับใจ
    • สงสัยว่านักพัฒนา Rust/LLVM จะทำให้ optimization นี้ถูกใช้โดยอัตโนมัติได้หรือไม่ในกรณีที่ทำได้ โดยชี้ว่าใน Rust มีข้อมูลเกี่ยวกับการ initialize หน่วยความจำที่แม่นยำกว่ามาก
  • มองว่าถ้าทุกเงื่อนไขเท่ากัน โค้ดประเภท codec แบบนี้ควรไปอยู่ในภาษาอย่าง WUFFS หรือภาษาเฉพาะทางลักษณะใกล้เคียงมากกว่า Rust และรู้สึกว่าการแปลงโค้ดซับซ้อนแบบ dav1d ไปเป็น WUFFS นั้นยากกว่าการแปลโค้ด C เดิมเสียอีก ถึงอย่างนั้นก็ยังเห็นว่าความพยายามแบบนี้มีคุณค่าและคุ้มแก่การลงทุนในระดับอารยธรรม
    • อธิบายว่า WUFFS เหมาะกับการ parse คอนเทนเนอร์อย่าง Matroska, webm, mp4 แต่ไม่เหมาะกับวิดีโอดีโคเดอร์เลย เพราะไม่มี dynamic memory allocation จึงจัดการข้อมูลแบบไดนามิกได้ยาก และเน้นว่าวิดีโอโค้ดไม่ได้มีแค่การ parse ไฟล์ แต่ต้องจัดการสถานะแบบไดนามิกหลากหลายมาก
  • พูดลอยๆ ว่าอยากรู้ความคืบหน้าของเงินรางวัล rav1d และแสดงความรู้สึกเชื่อมโยงเมื่อเห็นว่ามีคนอื่นสงสัยคล้ายกัน
  • รู้สึกว่าบทความที่เปิดมาด้วยมีมสนุกๆ มักเป็นโพสต์ที่ดี และพูดถึงความเชื่อมโยงกับประเด็นดังเมื่อไม่นานนี้เรื่อง “Rav1d AV1 Decoder Rust optimization เงินรางวัล $20,000” พร้อมเพิ่มลิงก์ที่เกี่ยวข้อง https://news.ycombinator.com/item?id=43982238
    • มีความเห็นเชิงขำว่ากรณีนี้เป็นตัวอย่างของ Nominative determinism อย่างชัดเจน
  • พูดตามตรงว่าการ optimize อย่างแรกนั้นเป็นรูปแบบที่พบได้บ่อยและน่าจะหาเจอได้ง่ายถ้าใช้ perf เก่งๆ เลยค่อนข้างแปลกใจ ส่วนประเด็น zeroing นึกว่าได้คุยกันไปแล้วในโพสต์แรก optimization อย่างที่สองซับซ้อนและน่าสนใจกว่า แต่ก็ยังเป็นทิศทางที่ perf ชี้นำอยู่ดี จึงเตือนว่าอย่าประเมินประโยชน์ของเครื่องมือ perf ต่ำเกินไป
    • อธิบายให้ชัดว่าจริงๆ ไม่ได้ใช้แค่ perf อย่างเดียว แต่ยังอาศัยการทำ differential profiling ระหว่างเวอร์ชัน C และเวอร์ชัน Rust รวมถึงการจับคู่ด้วยตนเอง จึงระบุปัญหาได้ถูกต้อง แม้ perf จะมีฟังก์ชัน diff แต่ก็มีข้อจำกัดเพราะชื่อ symbol ต่างกันทำให้จับคู่อัตโนมัติได้ยาก
    • กล่าวว่ามุมมองนี้มาจากการใช้งานอุปกรณ์ Apple ที่ใช้ aarch64 และย้ำจากประสบการณ์ว่าคนที่มีพื้นหลังต่างกันมักจับจุดที่พอมองย้อนกลับไปแล้วเหมือนเป็นเรื่อง “ชัดอยู่แล้ว” ได้เร็ว
  • คาดเดาว่าเบื้องหลังที่บัญชีทวิตเตอร์ของ ffmpeg ต้องออกมาแสดงจุดยืนเรื่องประเด็น Rust เมื่อไม่นานนี้ น่าจะมีเรื่องนี้เกี่ยวข้องอยู่ พร้อมแชร์ลิงก์ทวีต https://x.com/ffmpeg/status/1924137645988356437?s=46
    • หลังอ่านบัญชีทวิตเตอร์ของ ffmpeg ก็รู้สึกลังเลกับการใช้ ffmpeg และเสียดายที่ไม่มีทางเลือกอื่น มองว่าชุมชนนักพัฒนามีความเป็นพิษสูง แม้ประสิทธิภาพสูงสุดอาจสำคัญ แต่ในสภาพแวดล้อมที่ต้องรับส่งข้อมูลกับภายนอก ffmpeg ก็มีโอกาสเจอช่องโหว่ระยะไกล (CVE) หลายครั้งต่อปี จึงเน้นว่าจำเป็นต้องมี sandbox ที่เข้มงวดด้านความปลอดภัย และควรมีจุดกึ่งกลางที่ช่วยให้สร้างโซลูชันที่ทั้งเร็วและปลอดภัยไปพร้อมกัน พร้อมแชร์ลิงก์ที่เกี่ยวข้อง https://ffmpeg.org/security.html
    • เสนอว่าปฏิกิริยาที่ดีกว่าคือโต้ตอบด้วยการปรับปรุงประสิทธิภาพของ dav1d โดยเปรียบว่าการเอาแต่ปรับสถิติก็ยังให้ความรู้สึกด้อยกว่าการทำลายสถิติจริงๆ และอธิบายแบบขำๆ ว่าทางแก้ที่แท้จริงคือผลลัพธ์ที่เร็วและพลิกเกมได้จริง