解法
以前加载大型 WebAssembly 模块是串行的:浏览器先把整个文件下载成一个 ArrayBuffer,交给 JavaScript 引擎,然后才开始编译。在快速网络下,瓶颈不再是网络,而是解码和编译的 CPU 时间,而且这些全在下载之后发生,主线程还得暂停。
那些显而易见的修复各有代价。单个优化编译器生成代码快,但启动慢。JavaScript 引擎在运行时惰性编译,省了工夫,但编译仍占主线程,还得等解析完成。而旧 API 只接受完整的 ArrayBuffer,所以文件没到齐没法提前编译。
Firefox 58 两头下手。流式编译把每个下载块直接喂给引擎,通过流式 API,而且因为 wasm 模块的代码区在数据区之前,编译可以在大量数据还在传输时就开始了。新增的分层编译器加了一个基线层,生成代码比优化编译器快 10-15 倍,跑起来只慢 2 倍左右,于是代码几乎立即就能执行;后台线程再用优化编译器重编整个模块,并热替换进去。两个编译器都按函数把工作分给多个核。
因为编译吞吐量已超过网络吞吐量,最后一条数据包落地时,代码也已就绪。Mozilla 把这一改动比作像解码图片那样加载 WebAssembly,而不是像解析 JavaScript:没有主线程编译暂停,也不用等全部数据。
What it achieved
把流入的字节直接喂给快速的基线编译器,代码边到边跑,后台优化后再热替换
为什么有效
编译第一个数据包能把下载时间藏进 CPU 工作里,而不是先下载再编译。
基线编译器快 10-15 倍,启动很快;约慢 2 倍的代码先跑着,优化编译在后台推进。
wasm 函数独立编译,工作量可以分散到多个核,而不是排在一个线程上。
模块的代码区放在数据区之前,让流式编译能做最耗时的部分,而有效载荷还在传。
每个改动都缩短启动时间,同时不改变最终运行的机器码,峰值性能不受影响。
可以借鉴什么
当瓶颈步骤要等全部输入,就从第一块数据开始做,先出一个便宜版本;在后台升级它,而不是阻塞等待。
后续
Firefox 58 于 2018 年 1 月推出了流式和分层编译器;发布说明引爆了 Hacker News 上 912 点的评论。Mozilla 的测试页面让用户在自己设备上实测编译速度,团队还表示已在为 HTTP 缓存已编译机器码打基础,以扩展同版发布的 JavaScript 字节码缓存。
从这里,追溯依据
参考来源
- Making WebAssembly even faster: Firefox's new streaming and tiering compiler hacks.mozilla.org
- Hacker News discussion: Making WebAssembly even faster news.ycombinator.com