ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Hermes IR 类型系统 Primitive-Bitmask 快速路径设计:TypeContext 类型运算的位掩码加速方案

Hermes IR 类型系统 Primitive-Bitmask 快速路径设计:TypeContext 类型运算的位掩码加速方案 语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载导读本文深入剖析 Hermes JavaScript 引擎专门为 React Native 优化的 JS 引擎在 IR Type v2 重构之后为补偿TypeContext类型代数运算性能回退而设计并落地的Primitive-Bitmask Fast Path原始类型位掩码快速路径。该方案在保持Type4 字节句柄与id_相等性不变式完全不变的前提下把unionTy/intersectTy/subtractTy/isSubsetOf等高频类型运算从「遍历并 intern union 臂」的慢路径降级为「几条位运算 一张 6×6 查表 一次 DenseMap 命中」的内联快路径最终把 TypeInference 相对旧位掩码实现的编译时回归从约 10% 压缩到 1%3%。读完本文你将掌握这套位掩码编码的设计取舍、数字族格number-family lattice的构建方式、原型阶段三种缓存变体的对比结论以及如何在hermesc -O -ftime-report下复现测量。背景IR Type v2 重构带来的性能回归从 2 字节内联位掩码到 4 字节句柄在 IR Type v2 重构之前Hermes 的Type是一个2 字节内联位掩码类型代数运算全部是廉价的位操作。v2 重构详见 doc/plans/ir-type/ir-type-system-v2-design.md将其替换为一个4 字节句柄uint32_t id_指向每个Module独立的TypeContext类型表类型运算变为非内联调用out-of-line callsunion/intersect 的规范化统一走DenseMapUnionInternKey, …intern 表其键是SmallVectoruint32_t, 8见 include/hermes/IR/TypeContext.h 中UnionInternKey的定义。于是曾经一个位与/位或就能完成的操作现在要经历展开 union 臂 → 排序去重 → 消除被包含臂 → 构造SmallVector键 → 查哈希表 → 可能新建条目的完整流程。实测回归数据设计文档中记录了用[TI-TIMER]内嵌于源码的计时脚手架Release 构建、5 次运行取中位数实测的编译时间OLD v2 之前的位掩码提交e91cc037fNEW static_hHEADBundleOLDNEW回归Fb4aBundle.js 201940MB4.49s5.32s18.6%map/bundle-new.js 202378MB1.79s2.02s12.7%fb4a marketplace 202652MB6.36s7.24s13.9%配套结果文档 doc/plans/ir-type/typeinference-primitive-fastpath-results.md 用统一的-ftime-report复测TypeInference 中位数时间回归为 9.7% / 11.7% / 10.8%。热点定位sample性能剖析把新增开销归因于两条路径unionTy→createUnionImpl即DenseMap/SmallVector的 intern 过程占主导次要热点是isSubsetOf/intersectTy。值得注意的是最热的每条指令级操作——fixpoint 收敛判定用的operator、getType/setType——本质就是内联的uint32相等比较与读写从来不是问题。问题只出在需要构造并查询 intern 键的代数运算上。这一剖析结论直接决定了快速路径的设计范围只加速类型代数不动句柄表示。设计目标与范围设计文档明确了两条边界Bar (i)测量级原型measurement-grade prototype。要求在 TypeInference 所覆盖的全部场景上语义精确一致保证收敛/迭代行为完全相同、计时可信但不移植非热点消费者不打磨诊断输出整数类型必须一等公民支持。因为下一步计划就是整数产出的类型推断Int32/Uint32/UInt31这些整数类型在快速路径中处理不允许被踢到慢路径范围外精化对象类型ClassInstance/Array/Tuple/Function/ExactObject 等继续走慢路径getKind/getUnionArms/print/迭代器等不做改动。可掩码集合12 位掩码的编码空间覆盖的 9 个互斥位 3 位数字族编码快速路径的掩码只覆盖「会流经类型推断代数」的 kind共 12 位9 个互斥disjoint位Undefined、Null、Boolean、BigInt、String、Symbol、Object、Empty、Uninit3 位数字族编码number-family code含义见下节格结构。在 include/hermes/IR/TypeContext.h 的type_internal命名空间中这些常量有精确的定义static constexpr unsigned kNumCodeBits 3; // 低位数字族格编码 static constexpr unsigned kKindBits 9; // 高位每 kind 一位 static constexpr unsigned kPrimMaskBits kNumCodeBits kKindBits; // 12 static constexpr uint16_t kNotMaskable 1u kPrimMaskBits; // 0x1000哨兵以及逐 kind 的位定义MaskBit和数字族编码NumCodeenum NumCode : uint16_t { NC_None 0, // 不含数字 NC_UInt31 1, // [0, 2^31) Int32 ∩ Uint32 NC_Int32 2, // [-2^31, 2^31) NC_Uint32 3, // [0, 2^32) NC_Int32OrUint32 4,// [-2^31, 2^32) Int32 ∪ Uint32 NC_Number 5, // 全部 fp64 };为什么 Empty/Uninit 也要进掩码因为 TDZ暂时性死区推断会操作它们Phi 指令会对它们做 unionThrowIf/UnionNarrowTrusted这类操作会从类型中subtract 掉 Empty。如果不给它们编码位这些运算就无法走快速路径。非掩码 kind回落既有慢路径语义正确但实际几乎不会被触发因为它们在推断中恒为已知、从不被推断、也从不与 JS 值合并Environment、FunctionCode、PrivateName、Bits32以及所有精化对象 kind。源码中leafKindToMask的default分支lib/IR/TypeContext.cpp精确地返回kNotMaskable表示这些 kind。打包索引与可达组合数打包后的缓存索引为(disjointBits 3) | numberCode总宽 12 位2^12 4096种可能由于合法数字编码只有 0..5实际可达组合约 3072 种。这一看似很大但实际极小的索引空间正是后面缓存变体对比的核心变量。数字族格Number-Family Lattice6 元素、三张 6×6 表数字子类型存在互相重叠Int32 ∩ Uint32 UInt31因此不能像其他 kind 那样用独立位编码必须用格。六个元素都是Numberfp64的子集Number code 5 | Int32 ∪ Uint32 code 4 Int32 与 Uint32 的 join / \ Int32 Uint32 code 2 / 3 \ / UInt31 code 1 Int32 ∩ Uint32即 meet | ∅ code 0对应实现是 lib/IR/TypeContext.cpp 中三张constexpr uint8_t [6][6]查表kNumberJoinunion最小上界如Int32与Uint32的 join 为Int32OrUint32code 4kNumberMeetintersect最大下界如Int32与Uint32的 meet 为UInt31code 1kNumberSubsubtract最紧可表示的超集例如Number − Int32 Number、Int32OrUint32 − Uint32 Int32。subset(a, b)可由kNumberMeet[a][b] a导出这也是 lib/IR/TypeContext.cpp 中isSubsetOf快速路径采用的做法不必单独维护第四张布尔表。关键不变量表只会产出六个合法编码0..5因此数字族运算构造即规范canonical-by-construction无需事后规范化。头文件里还有三条static_assert兜底kPrimMaskBits 16保证掩码与哨兵塞得进uint16_t、NC_Number kNumCodeMask编码放得进 3 位、MB_Uninit 1u (kPrimMaskBits - 1)互斥位恰好铺满高位。Type → Mask给类型表条目加一个primMask字段实现方案是在TypeEntry中新增uint16_t primMask字段include/hermes/IR/TypeContext.hstruct TypeEntry { TypeKind kind; uint16_t primMask{type_internal::kNotMaskable}; // ... union 载荷classInstance / array / tuple 等 };在每个条目创建点填充叶子可掩码 kind→ 对应位数字叶子映射为对应数字编码NoType空集→ 0union→ 若所有臂都可掩码互斥位 各臂互斥位 OR数字编码 各臂数字编码的join否则kNotMaskable见addUnionEntry的折叠逻辑lib/IR/TypeContext.cpp精化/内部创建者→kNotMaskable。leafKindToMaskconstexpr 映射函数与createLeaf负责叶子条目addUnionEntry负责动态 union 条目的掩码折叠。一个重要的工程细节读取primMask是免费的——它和.kind共享同一条entries_[id]缓存行读掩码不会引入额外的 cache miss。操作级快速路径位运算 查表 缓存命中每个热点操作都在函数顶部加一段快速路径只要任一操作数的primMask kNotMaskable就原样回落既有实现。查询类Queries这类操作完全不触碰缓存、不物化任何东西只有位测试 数字查表isSubsetOf(a, b)(da db) da kNumberMeet[na][nb] na其中da/db是互斥位段、na/nb是数字编码段lib/IR/TypeContext.cppisSubsetOf的实现正是如此areDisjoint(a, b)互斥位无交集 且kNumberMeet[na][nb] NC_None所有canBe*查询canBeNumber/canBeString/canBeObject/canBeNull/canBeUndefined/canBeBigInt/canBeBoolean/canBeSymbol/canBeEmpty/canBeUninit单条m MB_X或(m kNumCodeMask) ! NC_None谓词isPrimitive、canBePrimitive、isNonPtr、isKnownPrimitiveType也都是掩码位测试isKnownPrimitiveType还借助countPopulation统计互斥位个数判定恰好一个 JS 原始类型。构造类ConstructorsunionTy/intersectTy/subtractTy的快速路径都是先算结果掩码互斥位用OR/AND/~数字用join/meet/sub再lookupPrimMask(result)Type TypeContext::unionTy(Type a, Type b) { // ... 恒等 / NoType 短路 ... uint16_t ma entries_[a.id_].primMask; uint16_t mb entries_[b.id_].primMask; if (ma ! kNotMaskable mb ! kNotMaskable) { uint16_t dis (uint16_t)((ma | mb) ~kNumCodeMask); uint16_t nc kNumberJoin[ma kNumCodeMask][mb kNumCodeMask]; return lookupPrimMask((uint16_t)(dis | nc)); } // ... 慢路径isSubsetOf 短路 createUnionImpl ... }intersectTy与subtractTy结构相同ANDkNumberMeet~kNumberSub。注意subtractTy是保守近似Number − Int32无法更精确表示kNumberSub表返回Number与慢路径行为一致测试FastPathConstructors中EXPECT_EQ(num, tc.subtractTy(num, i32))验证了这一点。lookupPrimMask与materializePrimMasklookupPrimMask(m)查primCache_llvh::DenseMapuint32_t, Type命中直接返回未命中则调用materializePrimMask物化后插入缓存materializePrimMask(m)lib/IR/TypeContext.cpp把掩码解码回叶子 id数字编码 →Number/Int32/Uint32/UInt31/{Int32,Uint32}臂对再走既有的createUnionFromLeafArmsintern 通道。因此物化产物与慢路径产物必然是同一个规范 id。原型期的缓存抽象三种变体的对比与最终取舍三种变体原型阶段在掩码 → Type 缓存后面放了一个小接缝PrimMaskCache构建时可选择宏或模板标签各自生成独立二进制无运行时开销DenseMapDenseMapuint32_t, Type惰性填充。稀疏单uint32键远比当时的SmallVector键便宜Lazy flatstd::vectorType容量2^12初值kInvalidId查找即下标未命中 → 物化 存储Eager flat同样的数组在TypeContext构造时全量物化所有可达掩码查找是纯下标加载、永不失手。一次性构造代价按 Module 计单独上报不计入[TI-TIMER]的runOnModule计时。三种变体共享同一条 intern 例程因此行为完全一致差异只在查找代价/构造代价。实测结果-ftime-reportRelease3 次中位数变体Fb4a-2019map-2023mkt-2026OLD位掩码4.741s1.840s6.508sNEW baseline5.203s2.054s7.212sDenseMap4.851s1.895s6.575slazy-flat4.913s1.960s6.782seager-flat4.878s1.868s6.436s回归恢复比例NEW baseline → OLD 地板DenseMap 76%/74%/90%lazy-flat 63%/44%/61%eager-flat 70%/87%/110%110% 表示在 marketplace bundle 上反超了旧位掩码。关键发现工作集只有约 40–60 个掩码对 DenseMap 打点统计其填充规模后发现一次编译中实际创建的互异原始掩码只有 38–58 个Fb4a 43、map 38、marketplace 58而索引空间是 4096。这解释了最终排名DenseMap 把约 40–60 个活跃条目保持热在寥寥几条缓存行里局部性最佳flat 数组把这 40–60 个条目散布在 4096 槽 / 16KB 区间这就是 lazy-flat 反而落后于 DenseMap 的原因eager-flat 一次性物化约 3072 个掩码其中约 98% 从未被读取物化浪费但全程序编译下其一次性构造代价没有体现为回归。最终决策已上线最终上线的是 DenseMap 变体。flat/eager 变体和HERMES_PRIMCACHE构建期选择器只是原型测量脚手架已全部移除在约 40–60 的工作集规模下缓存局部性占主导flat 数组免哈希的优势是幻影而 DenseMap 还省内存。这也解释了为什么 shipped 代码里不存在PrimMaskCache接缝、HERMES_PRIMCACHE选择器或 eager 填充路径。规范形式不变量与安全性primMask字段和掩码缓存是摆在internTable_前面的纯加速器internTable_仍是规范真值的唯一来源一个类型是否可掩码由其 kind 集合确定性决定快慢两条路径都经由internTable_intern因此无论走哪条路径每个类型都恰有一个规范id_operator以及 fixpoint 收敛判定只比较id_完全不受影响Type::operator是constexpr bool operator(Type RHS) const { return id_ RHS.id_; }见 include/hermes/IR/TypeContext.h。配套测试 unittests/IR/TypeContextTest.cpp 中的WellKnownUnionsAreInterned还顺带验证了动态构建 well-known union 必须命中其规范 well-known id 的回归场景。正确性门槛Correctness Gate快速路径是纯优化因此验证标准非常硬核字节级等价对三个 bundle快速路径版本输出的.hbc必须与 baseline-NEW逐字节一致直接cmp输出文件收敛行为不变[TI-COUNTERS]放大倍数必须与 baseline-NEW 一致证明改变的只是单操作代价不是收敛行为既有单测全过TypeContextTest全部通过。结果文档确认所有变体在三个 bundle 上都产出与 baseline 字节一致的.hbc并通过 45 个TypeContextTest单元测试以及 type-inference / InstSimplify 的 lit 子集。测量方案复盘如何复现原型阶段的测量步骤最终数值改用内置-ftime-report获得先把当前的 NEWhermesc复制保存保留 baseline 二进制分别构建三种缓存变体对每个变体在三个 bundle 上跑[TI-TIMER]harness5 次取中位数对比 baseline-NEW5.32 / 2.02 / 7.24s与 OLD 地板4.49 / 1.79 / 6.36s记录 eager-flat 的构造开销输出 {OLD, NEW-baseline, NEWDenseMap, NEWlazy-flat, NEWeager-flat} × {三 bundle} 表格。最终落地版本用hermesc -O -ftime-report测量取 TypeInference pass 各管线调用行之和Release 构建3 次中位数。注意原型用的[TI-TIMER]/[TI-COUNTERS]脚手架位于lib/Optimizer/Scalar/TypeInference.cpp是一次性测量脚手架已随原型回收不是 shipped 变更的一部分-ftime-report走 PassManager 内置计时见 lib/Optimizer/PassManager/PassManager.cpp 中timeCompiler分支的TimerGroup所有五个二进制用统一方式测量、零源码插桩。落盘文件清单shippedinclude/hermes/IR/TypeContext.hTypeEntry::primMaskkNotMaskable哨兵掩码位/数字编码常量type_internal命名空间kNumPrimMasksllvh::DenseMapuint32_t, Type primCache_成员lookupPrimMask/materializePrimMask声明UNIT_TEST下暴露的testPrimMask/testLookupPrimMask白盒测试钩子lib/IR/TypeContext.cppkNumberJoin/kNumberMeet/kNumberSub三张 constexpr 表leafKindToMask热操作union/intersect/subtract、isSubsetOf/areDisjoint、全部 canBe* 查询、isPrimitive/canBePrimitive/isNonPtr/isKnownPrimitiveType 谓词的双操作数可掩码快速路径lookupPrimMaskDenseMap find/insertmaterializePrimMaskprimMask在TypeEntry::createLeaf与TypeContext::addUnionEntry中的填充unittests/IR/TypeContextTest.cppPrimMaskValues叶子/well-known union/AnyType 的掩码值、LookupPrimMaskRoundTrips物化往返与 intern 稳定性、FastPathConstructors/FastPathQueries/FastPathPredicates逐操作的快/慢等价性等测试。与 TypeInference 的衔接快速路径服务的消费者这套加速服务的核心消费者是 lib/Optimizer/Scalar/TypeInference.cpp 的TypeInferenceImpl入口hermes::createTypeInference()STATISTIC(NumTI, ...)统计被推断指令数。其流程文件头注释按变量/调用互相影响的函数分组 → 清空组内全部指令类型信息clearTypesInFunction先存prePassTypes_→ 反复推断直到无变化。在推断循环里unionTy/intersectTy/subtractTy被高频调用inferPhi对 Phi 的所有入边值做 unionnewTy typeCtx_.unionTy(input-getType(), newTy)一元/二元算术推断用unionTy(Type::createNumber(), mayBeBigInt)组合结果类型数字精度收窄用intersectTyTDZ 语义用subtractTy(type, Type::createEmpty())剔除 Empty。这也正是设计文档强调Empty/Uninit 必须可掩码整数必须一等公民的缘由——它们都是 TypeInference 代数中的高频参与者。快速路径上线后这些调用大多数只需一次位运算 查表 DenseMap 命中即可完成而非走createUnionImpl的完整 intern 流水线。后续待办Deferred设计文档明确列出原型之后的待办若收益证明值得生产化若日后把掩码直接嵌入句柄再移植非热点消费者与打印数字族格向 6 元素之外扩展未来精化若新增元素三张表随之增长TypeInference 自身的 worklist / no-clear-rerun 重构——这是独立且更大的杠杆结果文档指出 fixpoint 存在约 10–13× 的重推断放大快速路径并不解决它。对希望继续深入源码的读者建议按 doc/plans/ir-type/typeinference-primitive-fastpath-design.md → doc/plans/ir-type/typeinference-primitive-fastpath-results.md → include/hermes/IR/TypeContext.h → lib/IR/TypeContext.cpp → unittests/IR/TypeContextTest.cpp 的顺序阅读配套 doc/plans/ir-type/ir-type-system-v2-design.md 与 doc/plans/ir-type/ir-type-v2-implementation.md 可补齐 v2 重构的全貌。赞分享语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载相关推荐Hermes IR 类型系统深度解析从 2 字节位掩码到 TypeContext 类型表Hermes IR 类型系统深度解析从 2 字节位掩码到 TypeContext 类型表 本文基于仓库文档 doc/plans/ir type/ir type语言运行时编译器移动开发Hermes IR Type System v2 设计解读从 16 位位掩码到可携带 Flow 类型信息的类型表Hermes IR Type System v2 设计解读从 16 位位掩码到可携带 Flow 类型信息的类型表 导读 本文围绕 doc/plans/ir t语言运行时编译器移动开发Hermes IR 类型系统 v2 迁移实战从 2 字节位掩码到 4 字节类型表Phase 1 实施方案全解Hermes IR 类型系统 v2 迁移实战从 2 字节位掩码到 4 字节类型表Phase 1 实施方案全解 本文围绕 Hermes 编译器仓库中 pha语言运行时编译器移动开发创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表