November 26, 2023 by 严肃游戏
JavaScript 代码直转 C/C++:你怎么知道 JavaScript 是世界上最好的语言?
关于 React Native 内置 JS Engine Hermes 新 AOT 功能 Static Hermes 之体验与 Benchmarking
最近,React Native 的自制引擎 Hermes 宣布了一个令人欣喜的功能:Static Hermes,即可以将代码库可以静态编译的部分提前编译成原生语言(现在的 DEMO 中是可以转成 C)执行,这意味着它可以自适应的让你的 JS/TS 代码享受静态编译的性能提升,它与 WASM 相比,最大优势在于 Optional,即整个过程是可选的,会根据代码类型自动优化生成相应代码,因为正常情况下并不是所有代码都是可以静态转换的,如果你的代码里同时有 TypeScript/Flow 代码和 JS 代码,那么类型确定的 TS 代码可以提前被转译成 Native,而动态的无法提前转译的代码就原样保留,用户不需要自己做代码分割或其他配置变更,也不需要一个完整的 Runtime 来运行 Bytecode 什么的,非常完美。相比于之前的提前编译出 ByteCode 的方案,更往前走一些,直接输出最终代码。因为移动端目标平台的设备类型/ABI 是比较明确的,不会有太大问题。(当然这里还存在 App Store 禁止远程分发执行某些代码的情况,如果转成 Native 的话热更新可能会有问题,但是可以把这个特性当成时机提前的 JIT,在分发的时候还是以 JS 代码进行分发,到用户端第一次执行时,进行转译,保存结果,之后复用即可)
Static Hermes 的伟大之处在于在把 JS Engine 应用层的差异给抹掉了,他们内部做了很多脏活累活,开发者要使用的话只需要用他们提供的 Compiler 把代码编译即可,无缝享受优化。当然现在还只有一些 DEMO,什么时候能真正用上那得另外说了。(国产跨端框架 KPI++)
但是这也足以令人兴奋了,在开发阶段,开发者可以美美享受 JS 的动态开发调试能力,开发完毕后上线了还能转成 Native 获得原生执行的性能优势,简直两全其美。还有在很多时候是不需要在用户侧有动态执行的能力的,有远程 diff 更新足矣。
最让人佩服的是他们在短短几年间就做了一个完整的、适用于移动端场景的 JS 引擎,工程能力无比强大,非常值得我辈学习,建议阅读源码。
当然,理想很美好,但是现阶段的 Hermes 仍然还是存在不少未解决的问题的,我在翻了 Hermes Issues 之后才发现有一堆 BUG,竟然还在 RN 里跑了那么久,这是不是侧面说明了 JS Engine 的性能在某种程度上无关紧要,并没有那么多人会意识了 performance regression issue。
正常来讲,相比于 JavaScriptCore,在某些极端场景下的性能会差几倍,比如 JSON 序列化、Date 格式化转换等,不过差异并不是特别明显。好在维护者对 issue 的处理很积极,会持续跟进并解决性能上的各种问题。其实大部分情况下 hermes 在移动端的表现都比 JSC 好得多,目前的 bytecode 模式优势就已经很明显了,针对移动端优化还是有许多优势的。
以下是一些相对比较严重的 Bug
JSON.stringify() is 3x slower on Hermes than JSC #1008
Memory leak when using fetch requests in react native #1147
Benchmarks
这里跑了一个 Repo 横行对比了 (V8)Node.js、JavaScriptCore、quickjs、以及不同 flag 的 hermes(bytecode、static hermes untyped、static hermes typed)
| base.js | calc.js | json.js | md5.js | nbody.js | |
|---|---|---|---|---|---|
| V8 | 0ms | 410ms | 354ms | 198ms | 35ms |
| JavaScriptCore | 0ms | 271ms | 153ms | 179ms | 35ms |
| quickjs | 0ms | 6101ms | timeout | 3381ms | 1150ms |
| bytecode hermes | 0ms | 2085ms | 648ms | 2027ms | 841ms |
| static untyped hermes | 0ms | 2334ms | 546ms | 2504ms | 555ms |
| static typed hermes | 0ms | 4ms | 520ms | 2263ms | 53ms |
从结果可以看到,在针对可以完全转译成 native 的 calc.js 简单计算任务情况下,typed static hermes 的表现特别好,其他情况比如类型频繁转换的 md5.js 情况下性能表现一般,远比 V8 和 JavaScriptCore 差,而大型 JSON 序列号方面的话 Hermes 的表现也远不如 V8 和 JSC,而 quickjs 甚至超时得不到结果。综合的性能表现 rank 是:V8、JavaScriptCore、Hermes、QuickJS,这其中 Hermes 和 QuickJS 基本上是同数量级的,而 Static Hermes 在某些场景下接近 Native。
从体积来看,比较适合移动端场景的是 JavaScriptCore、Hermes 和 QuickJS,v8 的体积太大了。在移动端上的差异并不是很大。Hermes 的 Static Hermes 特别适合 FFI 场景,基本上无限接近 C/C++ 的性能表现。QuickJS 在某些场景下如果没有经过优化,表现特别差,但是话说回来 benchmark 其实是一个比较极端的场景,把差距放大了,实际的表现差别未必有多大。
QuickJS 还有一个无法拒绝的好处就是非常小巧,可以随意 embed 到任何地方,代码也很容易 做 binding,pure c。Hermes、JavaScriptCore、v8 的话编译链都一堆东西了。
总结一下,从实际表现来看,比较适合 JSI 的场景,类似于简单短小方法的 inline,提升特别明显,而对于 hermes 自身的意义更多来说是比之前的 Bytecode 模式能有更多的性能提升,对于其未来比较乐观。JavaScriptCore 的表现和 V8 相当,不用它的原因在于很难基于它去做自己的定制,它本身的复杂度和 V8 相当了。而使用 Hermes 或者 QuickJS 去可以很方便的去做自己的优化,而 rust 社区里也有一些其他 JS Engine 实现,相比之下的话还是差上面这些一截,因为比较 JS Engine 还是一个相当复杂的东西,要做优化基本也很依赖实际场景和人力,没那么多人使用的自然就很难有太多余地去优化了。
分析 Repo 见: