ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用 Static Hermes 声音类型改造 Richards 基准:从 436ms 到 81ms 的实战指南

用 Static Hermes 声音类型改造 Richards 基准:从 436ms 到 81ms 的实战指南 用 Static Hermes 声音类型改造 Richards 基准从 436ms 到 81ms 的实战指南【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址: https://gitcode.com/gh_mirrors/hermes/hermesRichards 基准模拟操作系统任务调度器是 V8 Octane 套件中典型的类多态 虚分发 链表操作密集型负载非常适合验证静态类型对 JS 引擎性能的影响。本指南以 Hermes 仓库中的 benchmarks/octane/richards-typed 实验项目为主线完整讲解如何把一份未经类型的 Richards 基准改写为符合 Static Hermes 声音类型sound typing规则的--typed代码并通过hermes解释器、-XjitJIT 与shermesAOT 编译器三条路径测量性能差异。读完本文你将掌握 Static Hermes 的类型注解规则、shermes编译流水线与常用调试开关以及如何借助-dump-sema、-dump-lir和汇编输出定位类型化代码中的真实性能瓶颈。项目背景一个可以直接对比的类型化实验场Richards 基准最初由 Martin Richards 用 BCPL 实现模拟操作系统内核的任务调度调度器维护一组任务控制块TaskControlBlock通过优先级链表调度 Idle / Worker / Handler / Device 四类任务并统计队列与挂起次数来校验运行正确性。richards-typed目录中的两个文件是这一实验的核心richards.js —— 原始未经类型版本取自 V8 Octane 套件约定不改动作为对照基线richards-typed.js —— 面向 Static Hermes--typed模式的类型化改写版本。两个文件唯一的结构性差异在于文件末尾的测试驱动harness原始 Octane 版本经由BenchmarkSuite驱动并输出分数而这里的两个文件都改为用Date.now()计时、循环COUNT次、打印毫秒数的简易 harness见 richards-typed.js。由于 harness 一致两份代码可以直接对比。类型化版本同时保留了正确性自检逻辑runRichards()执行完毕后校验scheduler.queueCount与scheduler.holdCount是否分别等于EXPECTED_QUEUE_COUNT2322与EXPECTED_HOLD_COUNT928不一致即抛出异常——这保证了类型化改造没有改变程序语义。工具链准备两条执行路径实验假设$SH_BUILD指向 Static Hermes 的 Release 构建目录。按仓库根目录 CLAUDE.md 的配置方式每个 shell 只需设置一次export SH_BUILD$(pwd)/cmake-build-release # 与项目根 CLAUDE.md 的默认配置一致构建产物中与本文相关的两个二进制二进制路径作用hermes$SH_BUILD/bin/hermes解释器interpreter可叠加 JITshermes$SH_BUILD/bin/shermesAOT 编译器把 JS 编译为原生可执行文件hermes解释器与 JIT 路径# 未类型化纯解释器 hermes richards.js # 类型化纯解释器 hermes --typed richards-typed.js # 叠加 JIT-Xjit hermes -Xjit richards.js hermes --typed -Xjit richards-typed.js--typed开关让解释器以 Static Hermes 声音类型模式解析和执行代码-Xjit则启用 JIT 代码生成两条路径正交可组合出四种模式。shermesAOT 原生编译路径shermes的使用方式类似 C 编译器默认产出a.out支持-o、-c、-S# 未类型化 shermes -o richards-untyped richards.js # 类型化 shermes --typed -o richards-typed richards-typed.js静态链接--static-link--static-link避免共享库间接跳转理论上性能更好但当前存在一个已知问题shermes不会自动把asmjit与boost_context库传给链接器需要手动通过-L/-l补齐shermes --typed --static-link \ -L$SH_BUILD/external/asmjit/asmjit -lasmjit \ -L$SH_BUILD/external/boost/boost_1_86_0/libs/context -lboost_context \ -o richards-typed-static richards-typed.js-L与-l会被透传给底层 C 编译器。shermes 编译流水线运行shermes -v可以看到实际执行的每一条命令。整个流水线分三步JS → C先编译到 C 源码临时文件系统 C 编译器/usr/bin/cc以-O3编译出原生二进制链接-lhermesvm与-lshermes_console。除最终可执行文件外还可以用-c编译为.o、-S输出 C 源码、-emit-asm输出汇编检查中间产物。这些开关在 tools/shermes/shermes.cpp 中有完整定义OutputLevel参数还额外支持-dump-ast、-dump-transpiled-ast、-dump-sema、-dump-ir、-dump-lir、-dump-ra、-dump-lra等分级输出-dump-lir与-S正是后面性能分析的核心工具。Static Hermes 类型规则把类型当成 C 那样强制Static Hermes 采用 Flow及 TypeScript的一个子集但语义不同类型是声音类型——由编译器强制执行而非仅作提示。改造过程中需要遵守以下规则。类型注解所有函数/方法的参数、返回类型和类字段都必须显式注解。局部变量在编译器可以从初始化表达式推断类型时通常可以省略推断适用于var x new Foo()—— 推断为Foovar x this.field—— 从字段声明的类型推断var x someTypedExpr—— 当右侧表达式类型已知时。但以下情况必须显式注解局部变量变量未初始化var v;会被推断为any初始化表达式可空而变量在窄化检查后应为非空例如先if (queue null) return再var next queue推断结果是?T而非T初始化表达式返回any例如Date.now()。可用-dump-sema与-dump-ir验证去掉某注解后推断类型是否改变作为逐步精简注解的依据。var x: number 0; function foo(a: number, b: string): boolean { ... }数组用T[]而非ArrayTvar arr: number[] [0, 0, 0, 0]; // 不要写成var arr: Arraynumber new Arraynumber(4);类ES6 class 字段声明Static Hermes 要求 ES6 类不能用构造函数 .prototype的写法所有字段必须在类体内声明类型class Foo { x: number; next: Foo | null; constructor(x: number) { this.x x; this.next null; } bar(y: number): number { return this.x y; } }继承extendssuper()类层次结构使用extends与super()虚分发正常工作class Base { run(packet: Packet | null): TaskControlBlock | null { throw new Error(abstract); } } class Derived extends Base { run(packet: Packet | null): TaskControlBlock | null { // override 实现 } }可空性T | null与?T可空类型写作T | null或等价的简写?T文档推荐优先使用简写var x: Packet | null null; var x: ?Packet null; // 等价推荐写法一个关键限制类型检查器不会跨语句窄化this.field必须借助局部变量完成窄化// 不行——while 条件之后 this.currentTcb 不会被窄化 while (this.currentTcb ! null) { this.currentTcb.doSomething(); // 类型错误可能是 null } // 正确——局部变量窄化生效 var tcb this.list; while (tcb ! null) { tcb.doSomething(); // OKtcb 已被窄化为非空 }严格相等与禁止隐式字符串转换一律使用/!而非/!。string number属于类型错误需要显式转换// 错误 count someNumber // 正确 count String(someNumber)已知警告console未在类型系统中声明使用console.log()会产生警告但运行时可以正常工作。源码对照类型化改造的落点对照两个版本的实现可以直观看到上述规则的实际应用类型化版本见 richards-typed.js常量模块级常量从var改为const并全部给出: number注解状态常量如STATE_SUSPENDED_RUNNABLE STATE_SUSPENDED | STATE_RUNNABLE允许引用其他常量值会在初始化时折叠类替代原型function Scheduler()与Scheduler.prototype.addTask ...改写为class Scheduler内的方法字段queueCount、holdCount、blocks、list、currentTcb、currentId全部在类体声明类型可空字段link: ?Packet、queue: ?Packet、currentTcb: ?TaskControlBlock、blocks: (?TaskControlBlock)[]等明确标注可空局部窄化schedule()主循环把this.list存入局部变量tcb后以while (tcb ! null)驱动循环每次迭代先this.currentTcb tcb再判断tcb.isHeldOrSuspended()或tcb tcb.run()正是文档所述局部变量窄化的典型形态显式转换错误消息拼接处写为count String(scheduler.queueCount)ArrayT弃用Packet.a2声明为number[]Scheduler.blocks声明为(?TaskControlBlock)[]。值得注意的语义保持细节release(id)返回类型是?TaskControlBlockrunRichards()返回voidthrow new Error(unreachable)出现在currentTcb/tcb经 null 检查后的路径上用于满足可空值使用前必须窄化的检查。性能基线与引擎横向对比CLAUDE.md记录了 400 次迭代下的四模式基线同一机器、单一运行模式未类型化类型化解释器436ms193msJIT-Xjit207ms83ms原生shermes266ms84ms原生静态链接--static-link254ms81ms同一文档的结论在 benchmarks.md 中扩展为引擎横向对比Apple Silicon macOS、单次运行、2026-04-24400 次迭代引擎耗时 (ms)相对倍数Node v24.4.1V8 JIT191.0xStatic Hermes 类型化原生653.4xStatic Hermes 类型化 JIT864.5xStatic Hermes 类型化解释器1899.9xStatic Hermes 未类型化 JIT20810.9xStatic Hermes 未类型化原生29515.5xStatic Hermes 未类型化解释器43322.8xNode v24.4.1--jitless51827.3xPorffor 0.61.13AOT→Wasm54928.9xQuickJS 2025-04-2684844.6x两个表格的关键结论一致声音类型化在解释器、JIT、原生三条路径上均带来约 3–4 倍提速而类型化后的原生/JIT 模式与未类型化解释器之间差距可达 5 倍以上。需要强调的是这些数字是文档记录的特定环境测量值硬件、构建版本、运行次数都会影响绝对值适合作为相对趋势参考而非跨机器承诺。深入优化从 LIR 与汇编看类型化代码的瓶颈optimization-notes.md 记录了基于shermes --typed -dump-lir与shermes --typed -S的逐级分析展示了把类型化从正确运行推进到再快 8%的完整思路。常量var/let/const无差别字面量才内联实验确认var、let、const对 IR、LIR 与性能均无影响。真正决定是否内联的是初始化表达式纯数字字面量会被内联为LIRLoadConstInst不进作用域、零开销const STATE_HELD: number 4; // 内联 const ID_DEVICE_A: number 4; // 内联 const COUNT: number 1000; // 内联而初始化时引用其他声明的常量不会被内联它们存储在作用域%VS1中每次使用都通过LoadFrameInst读取——虽然值本身在初始化时已折叠例如STATE_SUSPENDED_RUNNABLE存为字面量 3但仍无法替换为立即数const STATE_SUSPENDED_RUNNABLE: number STATE_SUSPENDED | STATE_RUNNABLE; // 不内联 const STATE_NOT_HELD: number ~STATE_HELD; // 不内联DATA_SIZE 4虽是纯字面量却同样不内联推断是因为它声明在两个表达式初始化常量之后落入了同一作用域区段。热路径中的检查指令对 LIR 的观察揭示了类型化代码中仍然存在的运行时开销schedule()主循环用局部变量tcb驱动经CheckedTypeCastInst把null|object窄化为object循环体内联了isHeldOrSuspended()并部分内联TCB.run()虚分发this.task.run(packet)依次经过PrLoadInst读this.task类型uninit|object→ThrowIfInst未初始化字段检查→TypedLoadParentInstPrLoadInst从原型取run方法→CallInst返回any→CheckedTypeCastInst把any收回null|objectuninit|object字段每次访问都生成ThrowIfInst任务子类的scheduler字段尤甚IdleTask.run 一处就有 3 次可空字段在某一分支做过 null 检查后后续访问仍要求CheckedTypeCastInst因为编译器不会跨this.field传播窄化——HandlerTask.run 中约有 20 个此类转换非内联常量每次使用都要GetParentScopeInst/GetClosureScopeInstLoadFrameInst几乎出现在每个热函数中。这些正是文档类型检查器不跨语句窄化 this.field请用局部变量规则的底层原因编译器把窄化证据局限于局部变量字段访问需要重新建立检查。汇编级发现NaN-boxed double 与对象槽位布局所有number都是 NaN-boxed 的 IEEE 754 double位运算如state STATE_HELD需要完整的 double→int32→and→int32→double 往返且每次转换前还有fcmp d0, d0的 NaN 守卫。state、id、priority本质是整数却为此付出全部开销而 Static Hermes 目前还不支持原生整数类型对象只有 5 个直接属性槽offset 0–4位于直接槽的属性编译为内联内存加载如ldr x10, [x10, #40]溢出到 offset 5 的属性则落入间接存储需要运行时函数调用bl __sh_prload_indirect。这对有 6 个字段的TaskControlBlock影响巨大state是最频繁访问的字段每次isHeldOrSuspended、markAsHeld、markAsRunnable都读它却恰好是唯一溢出到间接槽的字段。实验将TaskControlBlock中的stateoffset 5与taskoffset 4对调后每次调度循环最热的读路径从函数调用变成内联加载实测 72ms → 66ms约 8% 提升; 调整前offset 5 → 间接存储函数调用 mov w2, #0 bl __sh_prload_indirect ; 调整后offset 4 → 直接槽内联加载 ldr x9, [x9, #72]复现与进一步实验按以下步骤可完整复现本文所有结论# 1. 准备 Release 构建按根目录 CLAUDE.md 的配置方式 export SH_BUILD$(pwd)/cmake-build-release # 2. 四条执行路径 hermes benchmarks/octane/richards-typed/richards.js hermes --typed benchmarks/octane/richards-typed/richards-typed.js hermes -Xjit benchmarks/octane/richards-typed/richards.js hermes --typed -Xjit benchmarks/octane/richards-typed/richards-typed.js # 3. AOT 编译 shermes --typed -o richards-typed benchmarks/octane/richards-typed/richards-typed.js ./richards-typed # 4. 静态链接含已知 bug 的 workaround shermes --typed --static-link \ -L$SH_BUILD/external/asmjit/asmjit -lasmjit \ -L$SH_BUILD/external/boost/boost_1_86_0/libs/context -lboost_context \ -o richards-typed-static benchmarks/octane/richards-typed/richards-typed.js ./richards-typed-static # 5. 中间产物检查类型表 / IR / LIR / 汇编 / 实际命令 shermes --typed -dump-sema benchmarks/octane/richards-typed/richards-typed.js shermes --typed -dump-ir benchmarks/octane/richards-typed/richards-typed.js shermes --typed -dump-lir benchmarks/octane/richards-typed/richards-typed.js shermes --typed -S benchmarks/octane/richards-typed/richards-typed.js shermes -v --typed -o /tmp/r benchmarks/octane/richards-typed/richards-typed.js值得继续探索的方向包括复刻TaskControlBlock字段重排的 8% 优化、尝试消除uninit|object引发的ThrowIfInst、以及观察未来原生整数类型落地后state位运算开销的变化。类型化改造本身的完整脉络——从原始 richards.js 到 richards-typed.js、从 CLAUDE.md 的基线到 optimization-notes.md 的逐级优化——全部沉淀在benchmarks/octane/richards-typed目录中是一份可以直接对照、反复实验的完整样本。【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址: https://gitcode.com/gh_mirrors/hermes/hermes创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表