☰
Hermes widgets 基准测试全解析:类继承、数组方法与 Map 的引擎性能探针
2026/9/25 6:40:59 网站建设 项目流程
  • 语言运行时
  • 编译器
  • 移动开发

【免费下载链接】hermes

A JavaScript engine optimized for running React Native.

项目地址:https://gitcode.com/gh_mirrors/hermes/hermes
点击查看免费下载

导读

widgets benchmark 是 Hermes 仓库中一组以"组件树渲染与协调(reconcile/diff)"为载荷的基准测试,用于衡量 JavaScript 引擎在处理类继承、方法调用、字段查找、数组方法以及Map等常见对象密集型工作负载时的表现。本文以 benchmarks/widgets/README.md 为主干,结合仓库内的 Flow 源码、去类型化版本、ES5 降级产物与 C++ 对照实现,完整讲解该基准的目录结构、各变体差异、核心算法与运行方式,帮助你理解 Hermes 团队如何用这一组精心构造的测试来观察引擎行为。

这个基准在测什么:Feature 清单与设计意图

根据 benchmarks/widgets/README.md,widgets benchmark 主要覆盖两类语言特性:

  • classes:继承(inheritance)、方法(methods)、字段查找(field lookups);
  • array methods以及Map。

从源码看,这些特性被组织成一个模拟 UI 组件树渲染的最小应用:Button、Floater、Gltf、Container等 Widget 类通过继承Widget基类实现多态reduce,RenderNode用数组与扁平化(flatMap)操作维护渲染结果,Context用字符串拼接生成子树 key,app_runner.js则用Map与数组的map/filter/includes完成两棵渲染树的差异计算。因此这个基准本质上是对"小对象 + 多态方法 + 数组/Map 容器操作"密集场景的一次高强度压力测试,与 React Native 中组件树的实际行为模式高度相似。

目录结构总览:四种变体与两类子目录

仓库 benchmarks/widgets 下的核心目录如下:

目录说明
original/原始 Flow 类型注解版本(多模块,含import/export)
single-file/将 original 所有文件拼接成单个文件
single-file/cpp/C++ 对照实现
simple-classes/只使用 Flow 类、不含其他复杂类型的简化版本

同时,几乎每个变体下都存在两类按输出形态划分的子目录:

  • /stripped/子目录:每个变体都包含,存放去除了 Flow 类型注解的 JS,可以直接交给不识别 Flow 的引擎(如 v8)运行;
  • /es5/子目录:在存在的地方,存放降级到 ES5的代码。降级方式有讲究——对单个文件只使用 Babel,而对多模块则使用 Webpack(见 benchmarks/widgets/README.md)。

这种"同一逻辑、多种形态"的布局,让开发者可以对比:类型注解是否影响引擎行为、ES5 降级是否改变性能特征、C++ 基线能跑到多快——从而把 JS 引擎自身的开销与算法本身的成本分离开。

变体一:original——Flow 类型注解的多模块原版

benchmarks/widgets/original 是基准的源头,所有其他变体都由此派生。它由多个 ES Module 组成,模块之间通过import/export连接。

核心类层次:Widget → Button/Floater/Gltf/Container

基类Widget定义了一个抽象契约——reduce(ctx)把自身"归约"为一个渲染节点,默认实现直接抛错要求子类实现;ComposedWidget则在reduce中先调用render()取得子 widget 再委托给它的reduce(见 widget.js)。

四个具体 Widget 类定义在 widgets.js 中:

  • Button:持有num: number字段,reduce时构造NumberComponent;
  • Floater:与Button结构对称的另一个数值型 widget;
  • Gltf:持有path: string字段,reduce时构造StringComponent;
  • Container:持有children: Widget[]数组字段,reduce时先产出自己的组件,再用this.children.map(child => RenderNode.createForChild(ctx, child))递归处理所有子 widget。

这些类直接体现了基准要压测的"字段查找 + 方法调用 + 继承多态":每次reduce都要读取实例字段、做一次动态分派的方法调用,并创建新的组件对象。

组件与渲染节点:小对象与数组操作的温床

  • components.js 定义了NumberComponent = {x: number}、StringComponent = {x: string}与联合类型Component = NumberComponent | StringComponent;
  • render_node.js 定义RenderNode:持有key、id、components数组与children数组,用static idCounter生成全局递增 id,reduce()通过(this.children || []).flatMap(child => child.reduce())把整棵子树拍平成VirtualEntity[](即[number, Component[]]元组数组);
  • context.js 定义Context:维护key字符串与childCounter,createForChild会用父 key 拼接子 widget 的 key(缺省时取child.constructor.name+ 计数器)生成树状唯一 key。

值得注意:RenderNode.createForChild中ctx参数实际未被使用,这种"冗余参数"也暗示 benchmark 更关注运行开销而非代码优雅性——它刻意制造了大量可被引擎观察的对象分配与方法调用。

运行载荷:从 10 个 widget 到 25 个 widget 的 diff

app_runner.js 中的runTest是核心载荷,它完整模拟了一次"组件树更新 + 协调"过程:

  1. 渲染一棵10 个 widget的旧树:new TestApp(false).render()→reduce(oldCtx)→reduce()得到旧实体树;
  2. 渲染一棵25 个 widget的新树(前 5 个与旧树相同):new TestApp(true).render()→reduce(newCtx);
  3. 用reconcileRenderNode/reconcileChildren按 key 协调新旧树,复用以相同 key 存在的旧节点 id(oldNode.id);
  4. 用diffTrees对比新旧实体树:通过Map(mapEntitiesToComponents)和数组的map/filter/includes求出创建/删除的实体与组件集合。

可以看到,Map被用于"实体 id → 组件数组"的哈希映射,includes/filter被反复用于集合差运算——这正是 README 提到的 "array methods andMap" 特性的落地位置。

入口与计时:warmup + 5000 次迭代

main.mjs 提供了标准计时流程:先50 次runFullTest(false)预热(printDiff为false时跳过树序列化),再以Date.now()计时跑5000 次迭代,最后打印Date.now() - t1毫秒数与迭代次数。index.js 则把runTest挂到globalThis.runFullTest上,而 original/README.md 说明:main.js只是main.mjs的副本,仅供 Webpack 使用(Webpack 对.mjs的处理与 Babel 路径不同)。

变体二:stripped——用 Juno 去除 Flow 类型

original/stripped/README.md 解释了去类型版本的来龙去脉:这是用脚本strip.sh通过 Juno 完成的转换,产物是不含 Flow 注解的 JS,以便在 v8 上运行。运行方式原文明确为:

v8 main.mjs

同时该 README 提示:用 NodeJS 也可能可以运行,但似乎需要把所有文件重命名为.mjs扩展名,并同步修改 import 语句中的引用。stripped目录中的 strip.sh 就是类型剥离的复现脚本。由于simple-classes/stripped/与single-file/stripped/同样存在,说明"有类型 vs 无类型"的对比在三种变体下都成立——这为观察 Flow 类型系统(及其在 Hermes 中的处理)对运行性能的影响提供了直接对照。

变体三:es5——Babel 与 Webpack 的降级路线

/es5/子目录展示了把现代 JS(类、模块、flatMap等)降级到 ES5 的两种路径:

  • 单文件场景只用 Babel:见 single-file/es5/README.md,预生成的产物是widgets.js,如需重新生成,在该目录执行:
    npm install npm run build

    其配套 babelrc.json 与 package.json 定义了 Babel 的构建配置与依赖;

  • 多模块场景用 Webpack:见 original/es5 目录,其中 webpack.config.js 负责把多模块打包降级,产物连同 source map(widgets.js.map)一起输出。

对比这两个版本的产物形态(单文件 Babel 产物 vs Webpack 打包产物),可以评估打包工具对代码体积与运行开销的影响——这也是 README 明确指出的"lowering 方式随模块数量而变"的工程细节。

变体四:single-file——拼接单文件与 C++ 对照

拼接规则

single-file/README.md 给出单文件的生成方式:按"能用 Flow 通过类型检查"的顺序把original下所有文件拼接在一起,并删除所有import/export语句。这与stripped(去类型)不同——single-file/widgets.js保留了类型注解,只是抹掉了模块边界。

C++ 对照实现:衡量引擎开销的基线

single-file/cpp 提供了三个 C++ 程序,用于建立"算法本身的成本"基线:

  • widgets.cpp:忠实翻译 JS 语义,用std::vector与std::function模拟数组方法,手工实现arrayMap/arrayFilter/arrayIncludes/arrayForEach/arrayConcat,用继承(NumberComponent/StringComponent继承自带虚函数的Component)模拟多态;
  • widgets_fast.cpp:更"宽松"的快速版本(从命名与widgets_fast_int的差别看,放宽了对 JS 语义的严格模拟,以换取编译器的优化空间);
  • widgets_fast_int.cpp:在 fast 基础上把数值组件字段从double换成int(int x;),进一步贴近整数优化路径。

配套 Makefile 定义了 6 个构建目标:widgets、widgets_fast、widgets_fast_int各自独立构建,以及各自链接libfakemalloc.so的*_with_fakemalloc变体。fakemalloc.cpp是一个用-D_GNU_SOURCE+-ldl编译的共享库,通过拦截 malloc 相关调用模拟"真实"分配行为。编译命令统一为:

c++ widgets.cpp -o build/widgets -std=c++17 -O3 -w

make run会把三个程序各自跑两遍(本机一次、fakemalloc一次),便于观察 GC/内存分配器对结果的影响。从源码结构看,这种"JS 原版 / 快速版 / 整数版 × 有无自定义内存分配器"的矩阵,正是为了把 JS 引擎的解释执行、对象模型与 GC 开销从算法成本中剥离出来单独量化。

变体五:simple-classes——只保留类的极端简化

simple-classes/README.md 说明该版本刻意做了两处简化:

  • 没有static成员(对应 original 中RenderNode.idCounter这类静态字段被去掉或改写);
  • 没有 tuple 或 object 类型(对应 original 中VirtualEntity = [number, Component[]]元组与{x: number}对象字面量组件)。

simple-classes/widgets.js(及stripped/widgets.js)因此成为"纯类 + 纯数组/Map 方法"的最小化场景,用于隔离"复杂 Flow 类型与元组/对象字面量"这一变量,观察仅凭类机制本身引擎能跑出怎样的性能特征——它与 original 的对比结果直接回答"类型与对象字面量是否是性能瓶颈"。

如何上手运行与进一步探索

综合各 README 与源码,运行 widgets 基准的推荐路径如下:

  1. 去类型版本(最快验证引擎本身):进入 benchmarks/widgets/original/stripped,执行v8 main.mjs(NodeJS 需先把相关文件改为.mjs扩展名并同步 import 引用);
  2. Hermes 直接运行:Hermes 的 REPL/命令行工具可直接加载original或single-file下的 JS 入口(入口通过globalThis.runFullTest暴露,计时逻辑在 main.mjs 中,默认 50 次预热 + 5000 次迭代 +Date.now()计时);
  3. ES5 产物复现:在 single-file/es5 中执行npm install && npm run build重新生成widgets.js;多模块 ES5 由 original/es5 的 Webpack 配置负责;
  4. C++ 基线:在 benchmarks/widgets/single-file/cpp 执行make run(先make all亦可),对比 6 个二进制产物,即可获得算法成本的下界参考。

如需把 widgets 放入更大的基准集合,可参考 benchmarks/CMakeLists.txt 与 benchmarks 下其他同类负载(如raytracer、imgui-demo、sqlite-read),它们共享"JS 场景 + 类型化/非类型化变体 + C++ 对照"的编排思路;Hermes 官方基准运行框架可参考 benchmarks/bench-runner 与 agent-perf 目录中的工具链。

小结:为什么 widgets 值得单独研读

widgets 基准的核心价值在于用一套贴近真实 UI 应用的最小模型,同时压测引擎的多个关键子系统:类继承与多态分派(Widget.reduce动态分派)、字段查找与隐藏类(实例字段读写)、小对象分配(组件对象与渲染节点)、数组方法(map/filter/flatMap/includes/forEach)以及Map哈希表。而original / stripped / es5 / single-file / simple-classes / cpp的多维变体矩阵,让开发者可以逐步剥离"类型注解、模块化、ES5 降级、对象字面量"等变量,把性能差异精确定位到引擎实现的某一层。对希望深入理解 Hermes 对象模型与 GC 行为的读者来说,这是仓库中信息密度极高的起点之一。

  • 语言运行时
  • 编译器
  • 移动开发

【免费下载链接】hermes

A JavaScript engine optimized for running React Native.

项目地址:https://gitcode.com/gh_mirrors/hermes/hermes
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询