- Đội ngũ phát triển FFmpeg công bố mức cải thiện hiệu năng lên tới 100 lần nhờ mã assembly tự viết
- Bản vá lần này chỉ mang lại hiệu quả tăng tốc ở một số hàm cụ thể, không phải toàn bộ chương trình
- Khi CPU mới nhất hỗ trợ AVX512, hiệu năng tăng tới 100 lần; với AVX2, ghi nhận mức tăng 64%
- Tính năng được tăng tốc này chủ yếu áp dụng cho một bộ lọc ít được biết đến
- Tối ưu hóa tự động của trình biên dịch vẫn cho thấy khoảng cách hiệu năng so với mã assembly viết tay
Cải thiện hiệu năng của FFmpeg: Ý nghĩa thực sự của mức tăng tốc 100 lần
Bản vá mới nhất và các điểm cải tiến chính
- Đội ngũ dự án FFmpeg nhấn mạnh thành quả "tăng tốc gấp 100 lần" trên công cụ chuyển mã media mã nguồn mở đa nền tảng sau khi áp dụng mã assembly do chính họ viết tay
- Tuy nhiên, nhóm phát triển cũng giải thích rõ rằng tuyên bố này chỉ áp dụng cho một hàm đơn lẻ, chứ không phải toàn bộ FFmpeg
- Việc tối ưu hóa nổi bật này được thực hiện trong hàm
rangedetect8_avx512, mang lại mức tăng tối đa 100 lần trên bộ xử lý hỗ trợ AVX512 mới nhất, và khoảng 64% trên nhánh AVX2
- Tính năng này được áp dụng cho một bộ lọc ít người biết tới; trước đây nó không nằm trong nhóm ưu tiên phát triển, nhưng lần này đã được tối ưu xử lý song song theo phương thức SIMD (single instruction multiple data)
Giải thích từ nhóm phát triển và bối cảnh kỹ thuật
- Qua Twitter, đội ngũ FFmpeg nói rõ rằng "chính hàm này đã nhanh hơn 100 lần, chứ không phải toàn bộ FFmpeg"
- Trong phần giải thích bổ sung, họ cho biết tùy hệ thống mà tính năng này có thể mang lại mức tăng tốc lên tới 100%
- Cải thiện hiệu năng này là kết quả của công nghệ SIMD, giúp nâng mạnh hiệu quả xử lý song song trên các chip hiện đại
Tầm quan trọng của việc tự viết assembly
Cách tiếp cận tối ưu hóa trong quá khứ và hiện tại
- Trong thập niên 1980~1990, ở môi trường phần cứng hạn chế, mã assembly viết tay là công cụ cốt lõi để tăng tốc game và nhiều loại phần mềm khác
- Ngày nay, hầu hết trình biên dịch hiện đại đều chuyển đổi mã ngôn ngữ bậc cao sang assembly, nhưng do giới hạn tối ưu hóa của trình biên dịch như phân bổ thanh ghi, mã assembly viết tay vẫn có thể cho hiệu năng cao hơn
- FFmpeg là một trong số ít dự án vẫn theo đuổi triết lý tối ưu hóa này, thậm chí còn có khóa học assembly riêng
Ảnh hưởng của FFmpeg trong hệ sinh thái
- Thư viện và công cụ của FFmpeg hoạt động trên nhiều môi trường như Linux, Mac OS X, Microsoft Windows, BSD và Solaris
- Các trình phát video phổ biến như VLC cũng sử dụng các thư viện
libavcodec, libavformat của FFmpeg
- Điều đó cho thấy FFmpeg có tầm quan trọng kỹ thuật lớn trong hệ sinh thái mã nguồn mở về mã hóa/giải mã media rộng lớn
Kết luận
- Dù lần tăng tốc này chỉ giới hạn ở một phần hàm, chứ không phải toàn bộ tính năng cốt lõi, đây vẫn là ví dụ cho thấy khả năng phá vỡ giới hạn hiệu năng
- Sự kết hợp giữa tối ưu hóa chuyên biệt cho phần cứng hiện đại và tinh thần mã nguồn mở tiếp tục giúp FFmpeg trở thành hình mẫu kỹ thuật trong lĩnh vực xử lý media
3 bình luận
Điều này đúng cả trước đây lẫn bây giờ
Tương tự, khi port một thư viện codec sang ARM, tôi cũng bắt đầu từ việc gỡ từng kernel viết bằng SSE, và sau khi gỡ xong, chỉ còn lại phần scalar rồi chạy benchmark trong môi trường thực tế thì chênh lệch hiệu năng là khá đáng kể
Đúng là những kỹ sư có thể viết mã còn được tối ưu hơn cả Gcc... thật đáng nể.
Ý kiến trên Hacker News
Tôi đã làm tối ưu hóa SIMD cho HEVC v.v. suốt 10 năm, và việc so sánh bản assembly với bản C thông thường gần như là chuyện đùa, vì đôi khi ra những con số khổng lồ như chênh lệch 100 lần. Thực ra kết quả như vậy thường có nghĩa là hiệu quả ban đầu cực kỳ thấp. Trong sử dụng thực tế, môi trường không giống microbenchmark, nơi cùng một hàm được gọi lặp đi lặp lại hàng triệu lần trong trạng thái cache đã đầy. Thay vào đó, trong tình huống thật, nó có thể chỉ được gọi một lần giữa nhiều tác vụ khác. Muốn giảm hiệu ứng cache thì phải tạo một vùng bộ nhớ kiểm thử rất lớn, nhưng tôi cũng không chắc người ta có thực sự làm vậy không.
Tôi cũng tò mò không biết phần mềm chuyển mã video như FFmpeg có tiến hành riêng các "macro benchmark" hay không. Có vẻ họ sẽ phải đo hiệu năng, chất lượng, mức dùng CPU trong thời gian dài với nhiều loại video và tổ hợp chuyển đổi khác nhau; mà để làm vậy chắc cần phần cứng chuyên dụng và nhất quán.
Hơi lạc chủ đề một chút, nhưng vì có vẻ bạn có nhiều kinh nghiệm với SIMD nên tôi muốn hỏi là bạn đã dùng ISPC chưa, và bạn nghĩ sao về nó. Tôi vẫn thấy khá vô lý khi đến giờ người ta vẫn phải tự viết mã SIMD, còn compiler thông thường thì vẫn yếu ở tự động vector hóa. Điều này trái ngược với kernel GPU, nơi từ lâu gần như lúc nào cũng được vector hóa tự động.
Tôi nghĩ bản thân ffmpeg cũng không khác microbenchmark là mấy. Về bản chất nó có dạng
while (read(buf)) write(transform(buf)).Đáng tiếc là ngoài vấn đề cache, đôi khi các nhà phát triển nói là tăng tốc 100%, nhưng thực tế chỉ áp dụng cho một bộ lọc rất hạn chế. Dù vậy, nhìn chung họ vẫn truyền đạt thông tin khá minh bạch.
Về cách diễn giải rằng "ban đầu kém hiệu quả", tôi nghĩ điều quan trọng là kết quả cuối cùng và những con số thực tế cho ra là gì.
Bài báo có chỗ nói 100 lần, chỗ khác lại nói tăng tốc 100%, nên hơi gây nhầm lẫn. Ví dụ họ viết rằng hiệu năng của
rangedetect8_avx512tăng 100.73%, nhưng ảnh chụp màn hình lại cho thấy 100.73 lần. Nếu là 100 lần thì tương đương tăng 9900%, còn tăng tốc 100% thì chỉ là nhanh gấp 2. Tôi muốn biết rốt cuộc cái nào mới đúng.Như trong ảnh chụp màn hình, rõ ràng là 100 lần (hay 100.73 lần) mới đúng, tức là tăng tốc 9973%. Có vẻ bài viết đã dùng sai ký hiệu
%.Tính theo một hàm riêng lẻ thì là 100 lần, còn tính theo toàn bộ bộ lọc thì là 100% (nhanh gấp 2).
Phía ffmpeg đang nói là 100 lần. Con số 100% có lẽ là lỗi đánh máy trong bài báo.
Tên hàm có liên quan đến
8, và nếu nó xử lý các giá trị 8-bit thì trong trường hợp bản cũ là scalar, AVX512 có thể xử lý 128 phần tử cùng lúc, nên tôi nghĩ tăng tốc 100 lần là hoàn toàn có thể.FFmpeg cũng có tài liệu hướng dẫn về cách viết assembly, để lại đây làm tài liệu tham khảo: https://news.ycombinator.com/item?id=43140614
Tôi cảm thấy vẫn chưa rõ
rangedetect8_avx512thực sự được dùng trong tình huống nào, và mức cải thiện hiệu năng thời gian thực trong toàn bộ quá trình chuyển đổi là bao nhiêu. Tôi muốn biết liệu điều này có thật sự mang ý nghĩa thực tiễn đủ rõ rệt hay không.Ngày xưa khi tín hiệu video còn là analog, người ta mã hóa tín hiệu điều khiển trong dải tín hiệu để xử lý. Sau thời DVD, tín hiệu số vẫn được xuất ra dạng analog, nên các mức màu dưới 16 (trong thang 0~255) được dùng làm tín hiệu "đen hơn màu đen"; BluRay và HDMI cũng tương tự. Gần đây xu hướng là dùng toàn bộ dải 0~255. Tuy nhiên, việc phân biệt dải tín hiệu vẫn thường bị xử lý sai khiến hình ảnh bị tối hỏng. Hàm này có vẻ dùng để xác định xem giá trị pixel thuộc dải 16~255 hay 0~255. Nếu chắc chắn là 16~255 thì có thể tiết kiệm thông tin khi mã hóa. Nhưng đây chỉ là suy đoán thôi. Tôi cũng làm công việc liên quan đến video, và thật xấu hổ là tôi vẫn hay xử lý sai mức đen.
Bộ lọc này không được dùng trong quá trình chuyển mã, mà được dùng để xác định thông tin cho metadata, chẳng hạn như dải màu hay alpha có phải là premultiplied hay không. Hàm đang được nói đến là phần dùng để xác định dải màu.
Tôi muốn nhắc tới một trải nghiệm thú vị. Lý do duy nhất tôi từng viết assembly cũng là vì SIMD. Gần đây tôi có nói chuyện lại về chuyện đó, và nhận ra mình đã quên mất tại sao hồi đó phải dùng assembly. Thực ra là do vấn đề aliasing trong compiler, khiến nó không tối ưu theo cách tôi muốn. Khi tôi chỉ rõ rằng dữ liệu sẽ không bị truy cập theo cách khác, và dùng một keyword không chuẩn, compiler tự động tận dụng được lệnh SIMD. Cuối cùng tôi bỏ luôn phần assembly tự viết.
Việc tối ưu hóa lần này chỉ áp dụng cho x86/x86-64 (AVX2, AVX512) có phần hơi mỉa mai. Trong thời kỳ mọi người đều dùng x86, SIMD optimization có khả năng được áp dụng rộng rãi, nhưng các kiến trúc mở rộng mới thì hoặc quá tệ, hoặc thiếu tương thích. Đến khi SIMD của x86 cuối cùng cũng trở nên tốt hơn, thì x86 lại không còn là chuẩn mặc định nữa, nên việc phụ thuộc vào nó trở nên khó khăn hơn.
Thực ra tôi khá ngạc nhiên khi assembly lại nhanh hơn C đã tối ưu. Compiler ngày nay giỏi đến mức tôi tưởng rằng tự viết assembly cũng chỉ giúp nhanh hơn chút xíu thôi, nhưng rõ ràng tôi đã nhầm. Giờ tôi quyết tâm là một ngày nào đó phải học assembly cho nghiêm túc.
Xem các patch liên quan thì phần baseline hiện có (
ff_detect_range_c) là mã C scalar rất tổng quát, còn phần tăng tốc đến từ bản AVX-512 của cùng phép toán (ff_detect_rangeb_avx512). Các nhà phát triển FFmpeg thích tự viết assembly bằng macro không phụ thuộc độ rộng vector, nhưng nhìn qua thì dùng intrinsics của Intel cũng có thể biểu diễn gần như tương tự (rốt cuộc khác biệt lớn nhất chỉ là phân bổ thanh ghi, nên không có khác biệt thực chất đáng kể). Cốt lõi của chênh lệch tốc độ là mức độ vector hóa tốt đến đâu. Với compiler hiện đại, vector hóa các vòng lặp không hề trivial gần như là bất khả thi, mà ngay cả vậy cũng phải bật tùy chọn nhưgcc -O3thì nó mới thử làm. Vì thế, với những phép toán đơn vị nhỏ như 8-bit này, nếu tự vector hóa bằng AVX/AVX2/AVX-512 thì ít nhất cũng thường nhanh hơn vài chục lần. Trên CPU hiện đại, cũng có những trường hợp có thể viết mã scalar cực tối ưu còn nhanh hơn compiler, nhưng rất hiếm và đòi hỏi chăm chút cực nhiều (chuỗi phụ thuộc, tải trên execution port, v.v.). Bản thân tôi cũng từng thấy mức tăng tốc 40%. Liên kết liên quan: baseline C, bản AVX512Khi đi sâu hơn vào tối ưu hóa mức thấp, bạn sẽ gặp các trường hợp compiler C bỗng tạo ra hành vi kỳ quặc chỉ trong chưa đầy một giờ. Nếu đó thực sự là đoạn mã được gọi cực kỳ thường xuyên thì các vấn đề tối ưu hóa đúng là có tác động rất lớn trong thực tế. Ví dụ: https://stackoverflow.com/questions/71343461/how-does-gcc-not-clang-make-this-optimization-deciding-that-a-store-to-one-str
Chỉ cần hạ xuống dùng SIMD intrinsics thôi cũng đã dễ kéo hiệu năng lên cao hơn compiler rất nhiều rồi. Tôi từng tự viết một loạt bài hướng dẫn chia thành nhiều phần về chuyện này: https://scallywag.software/vim/blog/simd-perlin-noise-i
Gần như mọi đoạn mã quyết định hiệu năng trong các thư viện C/C++ đều dùng assembly viết tay (ngay cả những hàm đơn giản như
strlencũng vậy). Compiler nhìn chung cho kết quả ổn trong đa số trường hợp, nhưng số phần mềm quan trọng đến mức đáng để đầu tư kiểu này thì không nhiều.Mức tăng tốc thực tế không đến từ assembly mà đến từ AVX512. Kernel này quá đơn giản, nên nếu viết bằng AVX512 intrinsics thì khác biệt giữa mã C và assembly gần như không đáng kể. Lý do của chênh lệch 100 lần là: a)
min/maxtrong SIMD được xử lý bằng một lệnh đơn, còn scalar thì tách thànhcmp+cmov; b) vì dùng độ chính xác 8-bit nên mỗi lệnh AVX512 xử lý được 64 phần tử cùng lúc. Kết quả là có thể bão hòa cả băng thông cache L1 và L2 (trên Zen 5 là 128B, 64B/cycle). Tuy nhiên, nếu xử lý cả một frame thì do kích thước bộ nhớ có thể phải truy cập L3, khi đó lợi ích sẽ giảm đi một nửa; và nếu còn vượt quá cả L3 thì lợi ích sẽ nhỏ hơn nữa.Tôi nhớ tới Sound Open Firmware (SOF), dự án này cũng có thể build bằng gcc hoặc compiler thương mại Cadence XCC (hỗ trợ Xtensa HiFi SIMD intrinsics): https://thesofproject.github.io/latest/introduction/index.html#toolchain-faq
Là 100x, không phải 100%.