- ทีมพัฒนา FFmpeg ประกาศว่าได้เพิ่มประสิทธิภาพสูงสุดถึง 100 เท่า ด้วย โค้ดแอสเซมบลีที่เขียนด้วยมือ
- แพตช์นี้ไม่ได้ทำให้ทั้งโปรแกรมเร็วขึ้น แต่ส่งผลเฉพาะกับ ฟังก์ชันบางตัว เท่านั้น
- เมื่อใช้กับ CPU รุ่นใหม่ที่รองรับ AVX512 พบว่าเร็วขึ้น 100 เท่า และบน AVX2 เร็วขึ้น 64%
- ฟีเจอร์ที่ได้รับการปรับปรุงนี้ ถูกนำไปใช้กับฟิลเตอร์ที่ไม่ค่อยเป็นที่รู้จักนักเป็นหลัก
- การปรับแต่งอัตโนมัติของคอมไพเลอร์ ยังแสดงให้เห็นช่องว่างด้านประสิทธิภาพเมื่อเทียบกับโค้ดแอสเซมบลีที่เขียนเอง
ความเร็วของ FFmpeg ที่เพิ่มขึ้น: ความหมายที่แท้จริงของการเร็วขึ้น 100 เท่า
แพตช์ล่าสุดและจุดปรับปรุงสำคัญ
- ทีมพัฒนาโครงการ FFmpeg เน้นย้ำผลลัพธ์ว่าเร็วขึ้น "100 เท่า" หลังนำ โค้ดแอสเซมบลีที่เขียนด้วยมือ มาใช้กับเครื่องมือแปลงรหัสสื่อโอเพนซอร์สแบบข้ามแพลตฟอร์ม
- อย่างไรก็ตาม ทีมพัฒนาอธิบายไว้อย่างชัดเจนว่า การอ้างนี้หมายถึง ฟังก์ชันเดี่ยว ไม่ใช่ FFmpeg ทั้งหมด
- การปรับแต่งที่โดดเด่นนี้เกิดขึ้นในฟังก์ชัน
rangedetect8_avx512 โดยทำได้สูงสุด 100 เท่าบน โปรเซสเซอร์ที่รองรับ AVX512 รุ่นใหม่ และดีขึ้นราว 64% บนเส้นทาง AVX2
- ฟีเจอร์นี้ถูกใช้กับ ฟิลเตอร์ที่ไม่ค่อยมีคนรู้จัก ซึ่งก่อนหน้านี้ไม่ได้ถูกจัดเป็นลำดับความสำคัญในการพัฒนา แต่ครั้งนี้ได้รับการปรับแต่งการประมวลผลแบบขนานด้วย SIMD (single instruction, multiple data)
คำอธิบายจากทีมพัฒนาและพื้นฐานทางเทคนิค
- ทีมพัฒนา FFmpeg ระบุผ่าน Twitter อย่างชัดเจนว่า "มีเพียงฟังก์ชันนี้ตัวเดียวที่เร็วขึ้น 100 เท่า ไม่ใช่ FFmpeg ทั้งหมด"
- ในคำอธิบายเพิ่มเติมยังระบุว่า ฟีเจอร์นี้อาจทำให้ความเร็วเพิ่มขึ้นได้ถึง 100% ขึ้นอยู่กับระบบ
- การเพิ่มประสิทธิภาพนี้เป็นผลจากเทคโนโลยี SIMD ที่ช่วยยกระดับประสิทธิภาพการประมวลผลแบบขนานบนชิปรุ่นใหม่อย่างมาก
ความสำคัญของการเขียนภาษาแอสเซมบลีด้วยมือ
แนวทางการปรับแต่งในอดีตและปัจจุบัน
- ในช่วงทศวรรษ 1980–1990 โค้ดแอสเซมบลีที่เขียนด้วยมือเป็นเครื่องมือสำคัญในการเพิ่มความเร็วของเกมและซอฟต์แวร์ต่าง ๆ ภายใต้ข้อจำกัดของฮาร์ดแวร์
- ปัจจุบัน แม้คอมไพเลอร์สมัยใหม่ส่วนใหญ่จะแปลงโค้ดภาษาระดับสูงเป็นแอสเซมบลีได้ แต่ด้วยข้อจำกัดด้านการปรับแต่ง เช่น การจัดสรรรีจิสเตอร์ โค้ดแอสเซมบลีที่เขียนเอง ก็ยังให้ประสิทธิภาพสูงกว่าอยู่
- FFmpeg เป็นหนึ่งในไม่กี่โครงการที่ยังยึดแนวคิดการปรับแต่งแบบนี้ไว้ และยังมี บทเรียนแอสเซมบลี ของตัวเองด้วย
อิทธิพลของ FFmpeg ต่อระบบนิเวศ
- ไลบรารีและเครื่องมือ ของ FFmpeg ทำงานได้บนสภาพแวดล้อมที่หลากหลาย เช่น Linux, Mac OS X, Microsoft Windows, BSD และ Solaris
- โปรแกรมเล่นวิดีโอยอดนิยมอย่าง VLC ก็ใช้ไลบรารี
libavcodec และ libavformat ของ FFmpeg
- สิ่งนี้สะท้อนให้เห็นถึงความสำคัญทางเทคนิคของ FFmpeg ภายใน ระบบนิเวศโอเพนซอร์สด้านการเข้ารหัส/ถอดรหัสสื่อ ที่กว้างขวาง
สรุป
- แม้การเพิ่มประสิทธิภาพครั้งนี้จะจำกัดอยู่ที่ บางฟังก์ชัน ไม่ใช่ความสามารถหลักทั้งหมด แต่ก็เป็นตัวอย่างที่แสดงให้เห็นการก้าวข้ามขีดจำกัดด้านประสิทธิภาพ
- การผสานกันของ การปรับแต่งเฉพาะสำหรับฮาร์ดแวร์รุ่นใหม่ กับจิตวิญญาณโอเพนซอร์ส ยังคงทำให้ FFmpeg เป็นแบบอย่างทางเทคนิคในสายงานประมวลผลสื่อ
3 ความคิดเห็น
เรื่องนี้ใช้ได้ทั้งเมื่อก่อนและตอนนี้
ในทำนองเดียวกัน ตอนพอร์ตไลบรารีโคเดกไปยัง ARM ผมก็เริ่มจากไล่แกะเคอร์เนลที่เขียนด้วย SSE ทีละตัว และเมื่อแกะจนหมดเหลือแค่สเกลาร์แล้วนำไปรันเบนช์มาร์กกับงานจริง ก็พบว่าความต่างด้านประสิทธิภาพมีนัยสำคัญ
เป็นวิศวกรที่เขียนโค้ดได้ปรับแต่งประสิทธิภาพยิ่งกว่า GCC เสียอีก... น่าทึ่งจริง ๆ
ความเห็นบน Hacker News
ตอนที่ผมทำ SIMD optimization สำหรับ HEVC และอย่างอื่นมา 10 ปี การเอาเวอร์ชัน assembly ไปเทียบกับเวอร์ชัน C ปกติแทบจะกลายเป็นมุกไปแล้ว เพราะบางทีมันต่างกันระดับ 100 เท่า ตัวเลขแรง ๆ แบบนี้จริง ๆ ก็หมายความว่าในตอนแรกมันไม่มีประสิทธิภาพอย่างมาก ในการใช้งานจริง สภาพแวดล้อมไม่ได้เป็นแบบ microbenchmark ที่แคชเต็มแล้วเรียกฟังก์ชันเดิมซ้ำหลายล้านครั้งตลอดเวลา แต่ในสถานการณ์จริง มันอาจถูกเรียกแค่ครั้งเดียวท่ามกลางงานอื่น ๆ มากมาย ถ้าจะลดผลของแคชก็ควรสร้างพื้นที่หน่วยความจำทดสอบขนาดใหญ่มาก ๆ มาทดสอบ ซึ่งก็ไม่แน่ใจว่าทำกันจริงหรือเปล่า
ผมก็สงสัยเหมือนกันว่าซอฟต์แวร์แปลงวิดีโออย่าง FFmpeg มีการทำ "macro benchmark" แยกต่างหากไหม ดูแล้วน่าจะต้องวัดประสิทธิภาพ คุณภาพ และการใช้ CPU เป็นเวลานานกับวิดีโอและชุดการแปลงหลายแบบ ซึ่งถ้าจะทำแบบนั้นก็น่าจะต้องมีฮาร์ดแวร์เฉพาะที่คงที่สม่ำเสมอ
ขอถามนอกประเด็นนิดหนึ่งนะ ดูเหมือนคุณจะมีประสบการณ์กับ SIMD มาก เลยอยากถามว่าเคยใช้ ISPC ไหม และคิดอย่างไรกับมัน ทุกวันนี้ยังต้องเขียน SIMD เองอยู่ และคอมไพเลอร์ทั่วไปก็ยังไม่เก่งเรื่อง auto-vectorization มันฟังดูแปลกอยู่พอสมควร โดยเฉพาะเมื่อเทียบกับ GPU kernel ที่มีการ vectorize อัตโนมัติมาตั้งนานแล้ว
ผมคิดว่า ffmpeg เองก็ไม่ได้ต่างจาก microbenchmark เท่าไรนัก โดยแก่นแล้วมันก็เป็นโครงสร้างแบบ
while (read(buf)) write(transform(buf))น่าเสียดายว่า นอกเหนือจากปัญหาเรื่องแคชแล้ว บางครั้งนักพัฒนาก็พูดว่าเร็วขึ้น 100% แต่จริง ๆ แล้วใช้ได้กับฟิลเตอร์ที่เฉพาะมาก ๆ เท่านั้น ถึงอย่างนั้นก็ยังถือว่าสื่อสารข้อมูลกันค่อนข้างโปร่งใสดี
สำหรับการตีความว่า "ตอนแรกไม่มีประสิทธิภาพ" ผมคิดว่าสิ่งสำคัญคือมันออกมาได้ตัวเลขเท่าไร และผลลัพธ์เองต่างหากที่สำคัญ
ในบทความมีทั้งที่บอกว่า 100 เท่า และบางจุดก็บอกว่าเร็วขึ้น 100% เลยชวนให้งง ตัวอย่างเช่นเขียนว่า "ประสิทธิภาพของ ‘rangedetect8_avx512’ เพิ่มขึ้น 100.73%" แต่ภาพหน้าจอกลับแสดง 100.73 เท่า ถ้า 100 เท่าจริงก็เท่ากับเพิ่มขึ้น 9900% ส่วนถ้าเร็วขึ้น 100% ก็คือเร็วขึ้น 2 เท่า อยากรู้ว่าจริง ๆ แล้วแบบไหนถูกต้อง
ตามภาพหน้าจอ เห็นได้ชัดว่าเป็น 100 เท่าแน่นอน (หรือ 100.73 เท่า) ซึ่งหมายถึงเพิ่มความเร็ว 9973% ดูเหมือนในเนื้อบทความจะใช้สัญลักษณ์ % ผิด
ถ้าวัดตามฟังก์ชันเดี่ยว ๆ คือ 100 เท่า แต่ถ้าวัดทั้งฟิลเตอร์คือ 100% (2 เท่า)
ฝั่ง ffmpeg อ้างว่า 100 เท่า ส่วน 100% น่าจะเป็นพิมพ์ผิดในบทความ
ชื่อฟังก์ชันเกี่ยวข้องกับ '8' และถ้ามันทำงานกับค่า 8 บิต การเพิ่มความเร็ว 100 เท่าก็เป็นไปได้ถ้า implementation เดิมเป็น scalar เพราะ AVX512 สามารถประมวลผลได้ 128 องค์ประกอบพร้อมกันในครั้งเดียว
ทาง ffmpeg เองก็มีคู่มือเกี่ยวกับการเขียน assembly อย่างเป็นทางการด้วย ฝากไว้เป็นข้อมูลอ้างอิง https://news.ycombinator.com/item?id=43140614
ในบทความยังรู้สึกไม่ชัดเจนว่า
rangedetect8_avx512ถูกใช้จริงในสถานการณ์ไหน และในกระบวนการแปลงทั้งหมดมันช่วยเพิ่มประสิทธิภาพแบบเรียลไทม์ได้มากน้อยแค่ไหน เลยสงสัยว่ามันมีความหมายเชิงปฏิบัติที่สังเกตได้จริงหรือไม่สมัยก่อนตอนสัญญาณวิดีโอยังเป็นอนาล็อก เขาจะเข้ารหัสสัญญาณควบคุมไว้ในย่านสัญญาณเดียวกันมาประมวลผล หลังยุค DVD แม้จะเป็นสัญญาณดิจิทัล แต่สุดท้ายก็ยังแสดงผลออกทางอนาล็อกอยู่ จึงใช้ค่าสีต่ำกว่า 16 (จาก 0~255) เป็นสัญญาณ "ดำยิ่งกว่าดำ" ส่วน BluRay และ HDMI ก็คล้ายกัน ช่วงหลังเริ่มมีแนวโน้มจะใช้ช่วง 0~255 ทั้งหมด แต่ทุกวันนี้ก็ยังแยกช่วงสัญญาณกันผิดจนภาพมืดพังอยู่บ่อย ๆ ฟังก์ชันนี้ดูเหมือนใช้ตรวจว่าค่าพิกเซลอยู่ในช่วง 16~255 หรือ 0~255 ถ้ารู้แน่ชัดว่าเป็น 16~255 ก็อาจช่วยประหยัดข้อมูลตอนเข้ารหัสได้ แต่ตรงนี้ผมเดาเอาเองนะ แม้ผมจะทำงานด้านวิดีโออยู่ แต่ก็ยังน่าอายที่มักจัดการ black level พลาดอยู่เสมอ
ฟิลเตอร์นี้ไม่ได้ถูกใช้ในกระบวนการแปลง แต่ใช้เพื่อตรวจข้อมูลเชิงเมตา เช่น color range หรือว่า alpha เป็นแบบ premultiplied หรือไม่ ฟังก์ชันที่เป็นประเด็นนี้อยู่ในส่วนตรวจ color range
อยากเล่าประสบการณ์ที่น่าสนใจ ผมเคยมีเหตุผลเดียวที่ต้องเขียน assembly ก็เพราะ SIMD เหมือนกัน ไม่นานมานี้ได้คุยเรื่องนี้แล้วเพิ่งนึกออกว่าทำไมตอนนั้นถึงต้องใช้ assembly ความจริงคือคอมไพเลอร์ optimize ไม่ได้ตามที่ต้องการเพราะปัญหา aliasing พอผมบอกให้ชัดเจนว่าข้อมูลจะไม่ถูกเข้าถึงในรูปแบบอื่น และใช้คีย์เวิร์ดนอกมาตรฐาน คอมไพเลอร์ก็ใช้คำสั่ง SIMD ให้อัตโนมัติ สุดท้ายผมก็ลบ assembly ที่เขียนเองทิ้ง
มันก็แอบน่าขันนิดหน่อยที่ optimization ครั้งนี้ใช้ได้เฉพาะกับ x86/x86-64 (AVX2, AVX512) ช่วงหนึ่งทุกคนใช้ x86 กันหมด เลยมีโอกาสที่ SIMD optimization จะแพร่หลาย แต่สถาปัตยกรรมส่วนขยายรุ่นใหม่ ๆ กลับแย่มากหรือไม่ก็เข้ากันได้ไม่ดี และพอ x86 SIMD เริ่มดีขึ้นจริง ตอนนี้ x86 ก็ไม่ใช่มาตรฐานหลักอีกต่อไปแล้ว เลยยิ่งพึ่งพาได้ยาก
จริง ๆ แล้วผมแปลกใจที่ assembly ยังเร็วกว่า C ที่ optimize แล้ว นึกว่าคอมไพเลอร์สมัยนี้เก่งมากจนการเขียน assembly เองคงเร็วกว่าแค่นิดเดียว แต่เห็นชัดเลยว่าผมเข้าใจผิดไป สักวันคงต้องตั้งใจเรียน assembly อย่างจริงจังแล้ว
ดูจากแพตช์ที่เกี่ยวข้อง implementation เดิม (
ff_detect_range_c) เป็น scalar C ที่ทั่วไปมาก และความเร็วที่เพิ่มขึ้นมาจากเวอร์ชัน AVX-512 (ff_detect_rangeb_avx512) ที่ทำงานเดียวกัน นักพัฒนา FFmpeg ชอบเขียน assembly ด้วยแมโครที่ไม่ผูกกับความกว้างเวกเตอร์ แต่ถ้าใช้ intrinsics ของ Intel ก็น่าจะเขียนออกมาได้แทบเหมือนกัน (สุดท้ายต่างกันหลัก ๆ แค่เรื่อง register allocation จึงแทบไม่มีผลเชิงปฏิบัติมากนัก) แก่นของความต่างด้านประสิทธิภาพคือทำ vectorization ได้ดีแค่ไหน คอมไพเลอร์สมัยใหม่แทบเป็นไปไม่ได้เลยที่จะ vectorize ลูปที่ไม่ trivial และถึงอย่างนั้นก็ยังต้องเปิด option ด้วย (gcc -O3เป็นต้น) ดังนั้นกับงานที่เป็นหน่วยเล็กอย่าง 8 บิตแบบนี้ ถ้า vectorize เองด้วย AVX/AVX2/AVX-512 ก็จะต่างกันอย่างน้อยหลายสิบเท่า บน CPU สมัยใหม่ บางครั้งก็ยังเขียน scalar code ที่ optimize สุด ๆ ให้เร็วกว่าคอมไพเลอร์ได้ แต่เกิดไม่บ่อยและต้องใส่ใจมากจริง ๆ (เช่น dependency chain และภาระบน execution port) ผมเองก็เคยได้เร็วขึ้น 40% มาแล้ว ลิงก์ที่เกี่ยวข้อง: baseline C, เวอร์ชัน AVX512ถ้าคุณลงไปแตะ optimization ระดับล่างมากขึ้นอีกหน่อย คุณจะเจอกรณีที่ C compiler ทำอะไรแปลก ๆ ภายในเวลาไม่ถึงชั่วโมง ถ้าเป็นโค้ดที่ถูกเรียกบ่อยมากจริง ๆ ประเด็นเรื่อง optimization จะสำคัญขึ้นมาทันที ตัวอย่าง: https://stackoverflow.com/questions/71343461/how-does-gcc-not-clang-make-this-optimization-deciding-that-a-store-to-one-str
แค่ลดระดับลงมาใช้ SIMD intrinsics ก็เพิ่มประสิทธิภาพได้ง่ายกว่าปล่อยให้คอมไพเลอร์จัดการเองมากแล้ว ผมเคยเขียนไกด์แยกเป็นหลายตอนเกี่ยวกับเรื่องนี้ด้วย https://scallywag.software/vim/blog/simd-perlin-noise-i
จุดที่เป็นตัวกำหนดประสิทธิภาพของไลบรารี C/C++ แทบทั้งหมดมักใช้ assembly ที่เขียนด้วยมือ (แม้แต่ฟังก์ชันง่าย ๆ อย่าง
strlenก็เช่นกัน) คอมไพเลอร์มักให้ผลลัพธ์ที่ใช้ได้ดีในกรณีส่วนใหญ่ แต่ซอฟต์แวร์ที่สำคัญมากพอจะคุ้มกับการลงทุนระดับนี้มีอยู่ไม่มากที่จริงแล้วความเร็วที่เพิ่มขึ้นไม่ได้มาจาก assembly แต่มาจาก AVX512 ต่างหาก เคอร์เนลนี้เรียบง่ายมากจนถ้าเขียนด้วย AVX512 intrinsics ความต่างระหว่าง C กับ assembly ก็แทบไม่มี เหตุผลที่ต่างกัน 100 เท่าคือ a)
min/maxบน SIMD ทำได้ด้วยคำสั่งเดียว ขณะที่ฝั่ง scalar ต้องแยกเป็นcmp+cmovb) ความละเอียด 8 บิตทำให้คำสั่ง AVX512 แต่ละคำสั่งประมวลผลได้พร้อมกัน 64 ค่า ผลลัพธ์คือสามารถใช้แบนด์วิดท์ของแคช L1 และ L2 ได้จนเต็ม (บน Zen 5 คือ 128B, 64B/cycle) แต่ถ้าประมวลผลทั้งเฟรม หน่วยความจำที่ใหญ่ขึ้นอาจบังคับให้ต้องเข้าถึง L3 ทำให้ประโยชน์ลดลงครึ่งหนึ่ง และถ้าไม่สามารถอยู่ใน L3 ได้เลย ประโยชน์ก็จะยิ่งน้อยลงทำให้นึกถึง Sound Open Firmware (SOF) ซึ่งก็สามารถเลือก build ด้วย gcc หรือคอมไพเลอร์เชิงพาณิชย์ Cadence XCC (รองรับ Xtensa HiFi SIMD intrinsics) ได้ https://thesofproject.github.io/latest/introduction/index.html#toolchain-faq
มันคือ 100x ไม่ใช่ 100% x