那么有没有正经的,“WASM 与所谓‘写的好一点’的 js”之间的对比呢?
www.sable.mcgill.ca/publications/techreports/2018-2/techrep.pdf WebAssembly and JavaScript Challenge: Numerical program performance using modern browser technologies and devices 这篇文章对比了同一套数值计算 benchmark 在 C 、WebAssembly 和 JavaScript 等不同平台下的性能,结果性能提升普遍在 1.5 到 2 倍之间。
当然,可以 argue 这个 benchmark 里面的 JS 并不是“最优化”的 JS (这个 JS 代码是他们重写过的,用了 Typed Array 之类的优化),真正“最优化”能做到什么程度呢?
这就要说回刚才那个答案了,答案回复里引用了一篇文章,干货比答案本身那是多得多:
https://zhuanlan.zhihu.com/p/102692865 一个白学家眼里的 WebAssembly (以下称为“白学文章”,因为实在是浓度过高 ...)
这个文章声称:“只要你懂得如何让 JS 引擎走在 Happy Path 上,那么在浏览器里,JS 就敢和 Rust 五五开。”
这里有个前提“JS 引擎走在 Happy Path 上”,这个的难度我前面说过。另外,在这篇文章的上下文中,Rust to WebAssembly 貌似无论是在文件大小、工具链完善程度和运行时性能还是上都没有 C to WebAssembly 更好,也就是换 C 可能就是四六开了 :)
精彩的是这个 claim 后面的证据,这是来自 Firefox 和 V8 两家浏览器引擎官方的内卷成果:
https://hacks.mozilla.org/2018/01/oxidizing-source-maps-with-rust-and-webassembly Oxidizing Source Maps with Rust and WebAssembly 这个作者 Nick Fitzgerald 应该是专门搞 Rust 和 WASM 的,立场应该有一定偏向。这篇文章是讲通过把 source-map 这个包里的 hot code 改成 使用 Rust 编译成的 WebAssembly 实现,在作者定义的一组 benchmark 上实现了成倍的性能提升。文章不短,不过主要是介绍 source-map 这个库,介绍 WASM 以及怎么用,怎么做 WASM 和 JS 之间的 binding,以及 benchmark 的结果。
前 V8 开发者 Vyacheslav Egorov 看了之后就写了一篇
https://mrale.ph/blog/2018/02/03/maybe-you-dont-need-rust-to-speed-up-your-js.html Maybe you don't need Rust and WASM to speed up your JS,他声称自己不站队,只是对优化感兴趣。这篇文章同样不短,全文大部分都是关于他怎么做的优化,包括:
1. 通过使用一个“*almost* default x64.release build of the V8”——也就是他重新 build 了一个 V8 来方便自己做分析,对 V8 引擎进行 profile,发现函数调用的时候 arity 不匹配或导致性能损失,改掉之后提升了一些性能。
2. 通过手动进行 monomorphisation,提升了更多的性能。
3. 通过分析发现原来包里中加入的 cache 并没有必要,删掉之后又提升了一截性能。
4. 把原来包里的内置排序算法改成桶排序,又是一截。类似的还有去除不必要的排序,并把快排换成插入排序。
5. 通过手动模拟静态类型语言的内存布局,优化 GC 。
6. 使用 Typed Array 继续优化(这个貌似需要重写整个逻辑,作者没有真的去重写只是预测了一下性能)。
做完这一切之后——V8 下性能差不多是刚才的 WASM 版本的 1.3-1.6 倍,而 SpiderMonkey ( Firefox 的 JS 引擎)下面的性能差距要小一点。
注意这六个主要优化中,1 是 V8-specific 的,这需要你了解 V8 的实现细节,3 和 4 是对程序算法本身的优化,和使用什么语言没有关系。2,5,6 则是 C/C++/Rust 等静态类型语言的语言特性自身就有的,而为了在 JS 中“模拟”这些特性,你需要这么写代码:
https://mrale.ph/blog/2018/02/03/maybe-you-dont-need-rust-to-speed-up-your-js.html#optimizing-parsing---reducing-gc-pressure之后 Nick Fitzgerald 又单独发了一篇文章:
https://fitzgeraldnick.com/2018/02/26/speed-without-wizardry.html Speed Without Wizardry 先吹了一下 Vyacheslav 的 expertise 和文章本身的质量,然后解释了下啥叫“wizardry”,最后把上面的 3 和 4 也在 WASM 版里面实现了,最后结果就是那个白学文章里面那个图:JS 和 WASM 在 V8 上完全打平,而在 SpiderMonkey 上,WASM 依然有明显的性能优势。
Nick 最后那篇文章有点阴阳怪气的意思,简单来说:但是,古尔丹,代价是什么呢?
标题的意思就是 Vyacheslav 的优化有一半属于“wizardry”,而使用 WASM 则完全不需要。他还特地摘出了 GC 优化那段,用 Vyacheslav 自己的话(“verbose and error prone code”)嘲讽了一番,然后大大的字体把 Vyacheslav 的代码 “memory[mappingA + 3] - memory[mappingB + 3]” “memory[mappingA + 4] - memory[mappingB + 4]” ... 贴出来。
(当然,他没有强调这代码之所以写成这德行,部分原因是照顾 SpiderMonkey 优化的一些短板)
他最后说:
> But a distinction between JavaScript and Rust+WebAssembly emerges when we consider the effort required to *attain* inlining and monomorphization, or to avoid allocations. Rust lets us explicitly state our desires to the compiler, we can rely on the optimizations occurring, and Rust’s natural idioms guide us towards fast code, so we don’t have to be performance wizards to get fast code. In JavaScript, on the other hand, we must communicate with oblique incantations to match each JavaScript engine’s JIT’s heuristics.
并且他同意 Vyacheslav 说的:
> Obviously each developer and each team are free to choose between spending N rigorous hours profiling and reading and thinking about their JavaScript code, or to spend M hours rewriting their stuff in a language X.
实际上,Vyacheslav 所做的优化,已经接近于重写代码了,我不认为这是“写的好一点”的问题。
这些具体的问题,那篇白学文章并没有细讲,白学文章自己推导出一个结论,就是“WASM 主要是方便把更复杂的原生应用直接搬进 Web”。其实完全不需要,WASM 自己的官方 paper (来自各大厂商的一线实现者)
dl.acm.org/doi/pdf/10.1145/3062341.3062363 Bringing the Web up to Speed with WebAssembly 就明确指出了
> The initial version of WebAssembly presented here focuses on supporting low-level code, specifically compiled from C/C++.
也就是设计上就不是让你从 JS port 过去的。之后才是
> A highly important goal is to provide access to the advanced and highly tuned garbage collectors that are built into all Web browsers, thus eliminating the main shortcoming relative to JavaScript when compiling to the Web.
而在折腾了这么半天之后,JS 版在 SpiderMonkey 上终究还是没打过 WASM 。吊诡的是哪怕 JS 版的性能,貌似 V8 也没打过 SpiderMonkey,证明 V8 并非是上天入地无所不能天下无敌的神器。
Vyacheslav 还有一些观点 Nick 并没有提:
* 他认为像上述 1 一样的 V8 的坑会越来越少。我个人对此不是很乐观,主要是在工具层面,而不是语言层面解决,依然是治标不治本的“wizardry”。
* 当然他也提到了从“language features”和“tools”层面一起解决。因为他也强调“Language and Optimizations Must Be Friends”,不是什么语言都可以随便优化的。
* 而其他的优化是“universal knowledge that spans across implementations and languages”,其中算法优化自不必说,而至于 JS 和 WASM 的对比,他没有给结论,只是用“N”和“M”一笔带过。并且强调“你都可以选择”,以及“language designers and implementors”都要加油奥利给。
* 以及“Clever Optimizations Must be Diagnosable"——对于 JS 引擎自身的 magic,浏览器并没有提供任何易用的接口,开发者基本没法知道我这个代码是不是什么地方惹 JS 引擎不爽了——应该不是每个前端开发者都会自己 build 一个 V8 的吧。这方面传统编译器反而做得要好得多,以 Clang 为例,最简单地,用户可以生成 optimization report 查看重要的优化有哪些做了哪些没做,再进一步可以查看生成的 LLVM IR 和 汇编,还可以看 IR 在每一个 pass 中的变化。GCC 和 Clang 传统编译器通常还会加入大量的 option 和 extension 方便性能优化。
这个涉及另外一个问题,就是通常 WASM 就是由某种形式的“传统编译器”生成的,这个编译器一般自身就是一个优化编译器。也就是说某种程度上讲,WASM 其实在编译器层面比 JS 更优越——它既有 V8 的婆罗门优化,又有传统编译器的刹帝利优化。之前说到 JIT 优化的不确定性,类似的问题在传统编译器中同样存在,区别是总的来说传统编译器做优化一般更容易,开发者可以访问更多优化的信息,并且实际性能受运行时情况的影响更小。总的来说 WASM 依然能够提供更好的 predictability 。
最后,在 V8 中,JS 和 WASM 都是通过 TurboFan 后端编译,但是在 SpiderMonkey 中,情况也是一样的。SpiderMonkey 的资料更少,但是其官方文档
https://firefox-source-docs.mozilla.org/js/index.html 说明,SpiderMonkey 最高级的 JS 后端,原来叫 IonMonkey,后来叫 WarpMonkey (最近改的名,其实 SpiderMonkey 这么多年已经养了一堆的猴儿了),但是 IR 还是叫 Ion MIR 和 Ion LIR 。而其最高级的 WASM 后端叫 BaldrMonkey,或 WASM-Ion,是“translates the WASM input into same MIR form that WarpMonkey uses and uses the IonBackend to optimize”。“IonBackend”具体指代什么不明,但是 IR 是一样的 IR 就基本是一样的后端。在 SpiderMonkey 的 WASM 实现源码中(
https://searchfox.org/mozilla-central/source/js/src/wasm/WasmIonCompile.cpp#5824 )一些关键模块,如 js::jit::OptimizeMIR 同样在 JS 编译时使用(
https://searchfox.org/mozilla-central/source/js/src/jit/Ion.cpp#1577 )。
https://github.com/bytecodealliance/wasmtime/blob/main/cranelift/spidermonkey.md 这里明确说 SpiderMonkey 中 WebAssembly 使用“IonMonkey-based tier 2 compiler”。前面提到的 WASM 的 paper,也包含了实现部分,是这么说的:
> V8 (the JavaScript engine in Google’s Chrome), SpiderMonkey (the JavaScript engine in Mozilla’s Firefox) and JavaScriptCore (the JavaScript engine in WebKit) reuse their optimizing JIT compilers to compile modules ahead-of-time before instantiation.
> The baseline JIT is designed only for fast startup while the Ion optimizing JIT is compiling the module in parallel in the background. The Ion JIT is also used by SpiderMonkey as its top tier for JavaScript.
> V8, SpiderMonkey, JavaScriptCore, and Chakra all include optimizing JITs for their top tier execution of JavaScript and reuse them for maximum peak performance of WebAssembly.
> Our experience reusing the advanced JITs from 4 different JavaScript engines has been a resounding success, allowing all engines to achieve high performance in a short time.
其实并不难理解,编译器本身差别并不大,既然已经有了一个非常不错的编译器,就没必要再写一个了。
最有意思的是把这个和之前的测试结果结合起来看:SpiderMonkey 同样给 JS 和 WASM 使用了一样的后端,然而这俩的性能确实差出了一截,可见一个语言的性能和编译器后端是啥根本就没有必然联系。