作者:quink
来源:Fun With FFmpeg
原文:https://mp.weixin.qq.com/s/GxGsMbXo1vQN93YdvzCQBA
一、什么是WebAssembly
WebAssembly是一种字节码的格式,这一概念一点都不新鲜,类似的有JVM和.NET的字节码。WebAssembly特殊在哪里?
- 浏览器支持运行WebAssembly程序
- 可以直接把C、C++,Rust程序编译成WebAssembly字节码
可以说,WebAssembly架起了一座桥梁,一边是大前端Web生态,一边是性能最高的编程语言(汇编你别吱声)。WebAssembly相比JS有两个优势:一个是性能,一个是可复用现有的C、C++和Rust代码。
除了浏览器,WebAssembly也扩展到了其他领域,作为一种沙盒机制。WebAssembly System Interface (WASI) 是一套接口标准,wasmtime、wasmer等是WASI接口标准的运行时,类似JVM。下面是一套从C代码到wasmtime运行WebAssembly的完整流程:
1. Hello FFmpeg程序
#include <stdio.h>
#include <stdlib.h>
int main()
{printf("Hello FFmpeg\n");}
2. wasi SDK把C程序编译成WebAssembly
$ wasi-sdk-24.0-x86_64-linux/bin/clang hello.c -o hello.wasm
3. wasmtime执行WebAssembly
$ wasmtime hello.wasm
Hello FFmpeg
二、WebAssembly SIMD加速方案
一般的纯计算类的C C++程序可以直接编译成WebAssembly。CPU密集型的任务,往往使用汇编加速。为了提升计算性能,WebAssembly也定义了自己的SIMD指令,见https://www.w3.org/TR/wasm-core-2/#vector-instructions%E2%91%A0。WebAssembly的simd128指令集,一次可以处理128 bit及16字节的数据。还有个在提案过程中的relaxed SIMD指令集。
Emscripten是WebAssembly的开创者,它列了5种使用WebAssembly SIMD的途径:https://emscripten.org/docs/porting/simd.html。归结下来是三种:
- 让编译器做自动的向量化编译
- 用WebAssembly SIMD intrinsics或者GCC/Clang的 SIMD向量扩展重新写一遍
- 让编译器把现有的X86或ARM的intrinsics自动翻译成WebAssembly
从省事上来说,方案1 > 方案3 > 方案2。从性能来说,方案2 > 方案3 > 方案1。
可能有人不了解什么是SIMD intrinsics。intrinsics是内在函数,SIMD intrinsics是一套对SIMD指令集的高级语言封装。例如,ARM NEON intrinsics的API见https://github.com/gcc-mirror/gcc/blob/master/gcc/config/arm/arm_neon.h。你不需要学习汇编语言,调用arm_neon.h C语言的API即可让编译器生成NEON指令,实现SIMD加速。
与SIMD intrinsics相对的是手写汇编,直接调机器指令来编程,也就是传统的汇编语言编程。手写汇编需要自己控制寄存器的使用,手动做指令重排等等。与SIMD intrinsics相比,手写汇编可以获得更高的性能。
注意,Emscripten列出的使用WebAssembly SIMD的方式,不包含手写汇编。WebAssembly是一种字节码格式,不是真实的机器指令。它没有寄存器,或者说无限寄存器。与手写汇编大约等效的是WebAssembly的text format格式。手写汇编可以面向特定CPU架构做优化,而WebAssembly运行的CPU是未知的,手写汇编里的各种优化方式,在WebAssembly场景用不上。所以,我认为WebAssembly SIMD正确的使用方式是SIMD intrinsics。开发者和编译器、WebAssembly运行时(V8,wasmtime等)是合作关系,而不是竞争关系。一部分优化工作交给编译器,一部分交给运行时的JIT等。
三、WebAssembly版FFmpeg的应用场景
Web的生态很强大,但Web的音视频处理能力很糟糕。有一些高层级的API,例如HTMLMediaElement、MSE、WebRTC,缺少底层控制支持。现在有了WebCodecs,但距离全量覆盖、功能完善还有距离。
自从有了WebAssembly,FFmpeg在Web端的音视频处理有了一席之地。WebAssembly版FFmpeg可以用来做:
- 封装解封装(容器格式)
- 音频编解码、filter
- 视频编解码、filter
前两个没什么疑问。容器格式处理是FFmpeg的强项,封装解封装需要的算力很小(相比于解码和图像处理)。音频编解码处理的数据量不大,WebAssembly版的FFmpeg处理起来没压力,不过还是有很大的优化空间,让性能更上一层楼。
最大的疑问在视频编解码。WebAssembly版FFmpeg做视频编解码,功能上可用,性能上远远不够。以速度著称的FFmpeg,为什么在WebAssembly上表现这么差呢?性能损失在哪里?
- 执行“WebAssembly字节码”与“C语言编译成机器语言运行” 相比的性能损失。这部分要靠编译器和WebAssembly的runtime去优化。
- 视频编解码重度依赖多线程并行处理,WebAssembly环境可能不支持多线程处理,丢失多线程并行加速。WebAssembly多线程的支持已经有了一些方案,可以把这部分性能损失找回来。
- FFmpeg现有的汇编代码全部不可用。前面说过,LLVM可以把ARM、X86的SIMD intrinsics翻译成WebAssembly SIMD。但FFmpeg为了更好的性能,只用手写汇编(龙芯架构除外),各个CPU架构的手写汇编没法直接翻译成WebAssembly SIMD。补上这块短板,需要用WebAssembly SIMD重写一遍汇编加速。
Web端的视频编解码,FFmpeg最大的潜在需求大概是H.265解码。新版本Chrome支持H.265硬解,但一方面存在版本覆盖问题,一方面存在硬解不可用问题。对于客户端App来说,软解H.265和硬解H.265方案的可行性都非常高,用哪个都可以,两个方案相结合覆盖95%以上的设备没问题(除非十多年前的老设备)。而Web端,各种方案捉襟见肘,一个能打的都没有,重度依赖回退H.264。
四、WebAssembly FFmpeg H.265解码的SIMD加速
根据前面的讨论,提升FFmpeg视频解码性能的关键在于用WebAssembly SIMD重写FFmpeg的汇编加速。技术方向没有疑问,但FFmpeg社区一直没有人做这份工作,我推测的原因有:
- FFmpeg视频解码不是Web端唯一的方案,也没法说是最好的方案。如果浏览器自己把H.265解码做好了,FFmpeg就变成了一个备胎兜底的角色
- 重写汇编加速的工作量很大,哪怕只是重写H.265的汇编加速
- FFmpeg开发者和Web开发者交集少
- 没有公司赞助
我等啊等啊等,等了三年不见动静,包括对WebAssembly的讨论都没有。我不做Web开发,但经常看到Web开发同学用FFmpeg。与其继续等下去,不如戳一戳社区,看看反应。
我写了一个H.265 IDCT(反离散余弦变换,严格来说H.265 IDCT是近似反离散余弦变换)的WebAssembly SIMD版本。patch见https://ffmpeg.org/pipermail/ffmpeg-devel/2024-November/336009.html。H.265 IDCT的大小有四种:4×4,8×8,16×16,32×32。WebAssembly SIMD加速效果如下:
hevc_idct_4x4_8_c: 20.4 ( 1.00x)
hevc_idct_4x4_8_simd128: 14.1 ( 1.44x)
hevc_idct_4x4_10_c: 17.9 ( 1.00x)
hevc_idct_4x4_10_simd128: 14.1 ( 1.27x)
hevc_idct_8x8_8_c: 232.3 ( 1.00x)
hevc_idct_8x8_8_simd128: 56.1 ( 4.14x)
hevc_idct_8x8_10_c: 222.1 ( 1.00x)
hevc_idct_8x8_10_simd128: 60.9 ( 3.65x)
hevc_idct_16x16_8_c: 1619.1 ( 1.00x)
hevc_idct_16x16_8_simd128: 384.6 ( 4.21x)
hevc_idct_16x16_10_c: 1543.1 ( 1.00x)
hevc_idct_16x16_10_simd128: 391.4 ( 3.94x)
hevc_idct_32x32_8_c: 18518.3 ( 1.00x)
hevc_idct_32x32_8_simd128: 2143.1 ( 8.64x)
hevc_idct_32x32_10_c: 17633.3 ( 1.00x)
hevc_idct_32x32_10_simd128: 2139.1 ( 8.24x)
带simd128后缀的是我实现的SIMD加速版本,带c后缀的是纯C逻辑实现的版本,注意c的版本是带了编译器自动向量化处理的。这里比较的是手写向量化和编译器自动向量化的效果。可以看到,8×8和16×16大约是4倍加速,32×32是8倍加速。4×4因为单次要处理的数据量小,加速比不高。
让我们看下仅仅优化一个函数对解码速度的影响。
配置 | 关闭SIMD | 编译器自动向量化 | 手写优化IDCT |
解码速度/FPS | 44 | 70 | 78 |
测试单线程解码,视频分辨率1080P,机器是Linux系统,CPU是Intel 12700。这里有三种配置:
- 彻底关闭WebAssembly SIMD,生成的字节码里不包含WebAssembly SIMD
- 只开启编译器自动向量化
- 开启编译器自动向量化,加上手写实现的WebAssembly SIMD版IDCT
可以看到,编译器自动向量化把解码速度提升了59%。从效果来说很显著,但另一方面来说,编译器只能做到这个水平了。手写优化IDCT,让解码速度进一步提升了11%,注意这只是优化了一个函数。
对比编译本地运行的FFmpeg,关闭手写汇编的速度:
$ ./ffmpeg -hide_banner \
-cpuflags 0 \
-threads 1 \
-i basketball-v265.mp4 \
-an -f null - \
-benchmark
frame= 500 fps=104 q=-0.0 Lsize=N/A time=00:00:20.00 bitrate=N/A speed=4.14x
bench: utime=4.865s stime=0.035s rtime=4.830s
开启手写汇编的速度:
$ ./ffmpeg -hide_banner \
-threads 1 \
-i basketball-v265.mp4 \
-an -f null - \
-benchmark
frame= 500 fps=213 q=-0.0 Lsize=N/A time=00:00:20.00 bitrate=N/A speed=8.52x bench: utime=2.380s stime=0.027s rtime=2.349s
在MacBook Pro M1芯片上测试,数据差不多,不开启手写汇编优化是118FPS,开启是212FPS。注意,不论是在Linux Intel芯片环境还是MacOS M1环境,我都是用的clang编译,默认开启了自动向量化。如果是用gcc编译,因为gcc向量化实现有bug,被FFmpeg关闭了向量化功能,没有手写汇编的情况下,单线程解码速度只有58 FPS,clang一半的速度。
不同编码配置解码速度也不同。我拿来做测试的视频,解码复杂度不低。我预估实现全部WebAssembly SIMD优化的FFmpeg H.265 1080P解码速度能达到140 FPS。如果是更简单的编码配置,解码速度可以更高。
五、展望
从测试效果来看,编译器自动向量化加速效果明显,手写加速效果更好,在Intel 12700和Apple M1上实现WebAssembly单线程解码1080P 140FPS问题不大。
剩下的就看这事值不值得投入。FFmpeg视频解码在Web上是不是一个备胎兜底的角色?从另一个角度来说,是不是优化WebAssembly FFmpeg音频处理的性能更有价值?我能决定做什么,但我不确定的是做完之后的价值。欢迎Web端音视频开发同学来探讨这个话题。
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。