# HHVM/HHBBC 编译器 Review:全程序分析、类型系统与优化管道 > 2026-06-11 · 基于 /home/swoole/workspace/cpp/hhvm 源码审查 --- ## 一、架构概览 HHVM 的编译体系分为三层: | 层级 | 组件 | 职责 | |------|------|------| | 前端 | HackC (hphp/hack) | Hack/PHP 源码 → HHAS (Hack Assembly) | | 中端 | HHBBC (hphp/hhbbc) | HHBC bytecode → 优化后 HHBC bytecode | | 后端 | JIT (hphp/runtime/vm/jit) | HHBC → x86-64 机器码 (运行时 tracing JIT) | HHBBC(~80k 行 C++)是一个**全程序字节码优化器**,基于不动点迭代分析。核心文件: | 文件 | 行数 | 用途 | |------|------|------| | `interp.cpp` | 6,685 | 抽象解释器(forward dataflow) | | `index.cpp` | 30,132 | 全程序索引:类型信息、依赖追踪、增量重分析 | | `type-system.h` | 1,610 | 类型 lattice 定义(trep + specialization) | | `type-system.cpp` | 8,451 | 类型操作实现(meet/join/subtype/union) | | `dce.cpp` | 3,102 | 类型感知的死代码消除(局部 + 全局) | | `analyze.cpp` | 2,293 | 函数级数据流分析驱动 | | `optimize.cpp` | 962 | 类型感知优化 pass | | `cfg-opts.cpp` | ~300 | CFG 优化(不可达块删除、异常边简化) | 与 AOT Compiler 的对比: | 维度 | HHBBC | AOT Compiler | |------|-------|-------------| | 编译目标 | HHBC → 优化 HHBC → JIT 机器码 | PHP → C++ → 二进制 | | 分析级别 | 字节码级(全程序) | AST 级(单文件) | | IR 形式 | php::Func (factored CFG + Bytecode) | PHP-Parser AST | | 类型推理 | trep + specialization + 不动点迭代 | SSA + manual type annotations | | 优化范围 | 全程序(跨文件/跨函数) | 单函数 + 类层次 | | 调用图 | 动态构建(Index + assumption) | 静态(classExtends + classMethodOverride) | | 并发 | 函数级并行分析 | 串行执行 | | 运行时 | HHVM(JIT + refcounting GC) | phpx(RAII 包装 Zend API) | | 代码量 | ~80k 行 C++(仅 HHBBC) | ~15k 行 PHP | --- ## 二、可借鉴的创新设计 ### 2.1 全程序不动点分析(Whole-Program Fixed-Point) **文件**: `hphp/hhbbc/README`, `analyze.cpp`, `index.cpp` HHBBC 的核心算法:在**全程序范围**内进行迭代收敛分析。 ``` 算法流程: 1. 初始化 work list = 所有函数/类 2. 并行分析每个 work unit(只读 Index,产出一个新结果) 3. 单线程:将新结果合并到 Index 4. 如果 Index 有更新 → 把依赖该信息的函数加回 work list 5. 重复直到 Index 不动点(信息不再变化) 6. 最终并行优化 pass ``` 关键设计原则: - Index 中的信息**只能收缩**(shrinking types:变得更精确) - 函数内分析的类型**只能增长**(growing types:在分析上下文中累积) - Index 信息**永远不会错误**(soundness guaratee)——只可能不够精确 **AOT 借鉴优先级: P0** 当前 AOT 是单文件串行编译,没有跨文件/跨函数分析。可以借鉴: 1. **Index 结构**:存储每个函数的返回类型、参数类型、属性类型等推断结果 2. **依赖追踪**:记录每次查询("函数 F 查询了类 C 的方法 M 的返回类型"),在 C::M 类型更新时将 F 加回 work list 3. **迭代收敛**:初始化所有函数为 "未知" → 逐轮分析 → 直至类型信息不再变化 实现建议: ```php // Preprocessor 中构建 class AnalysisIndex { // 函数返回类型(每轮迭代中收缩) public array $returnTypes = []; // funcName => TypeInfo // 依赖图:funcName => [被依赖的 funcName, ...] public array $dependencies = []; // 函数入参状态(从调用方收集) public array $paramTypes = []; // funcName => [argIdx => TypeInfo] // 公共静态属性状态 public array $publicStaticProps = []; // className::prop => TypeInfo } ``` 然后按拓扑顺序反复分析函数,直到 `$returnTypes` 和 `$paramTypes` 不变。 --- ### 2.2 Trep 类型格(Bitset Type Lattice) **文件**: `hphp/hhbbc/type-system.h`, `type-system-bits.h`, `type-system-detail.h` HHBBC 的类型系统是整个优化器的基石,设计精巧: **Base 机制 — trep (type representation):** ```cpp // 每个"基本类型"是一个 bit BUninit, BInitNull, BFalse, BTrue, BInt, BDbl, BCls, BLazyCls, BFunc, BClsMeth, BEnumClassLabel, BObj, BRes, BRFunc, BRClsMeth, BSStr, BCStr, // 不可数/可数字符串 BSVec, BCVec, BSVecE, BCVecE, BSVecN, BCVecN, // 数组的 counted × empty 维度 // ... Dict, Keyset 同理 ``` 类型通过 bitset 组合表示联合类型。例如 `Int|String` 就是 `BInt|BStr`。 **Specialization(特化)——类型附带的额外信息:** ``` Int=n — 已知常量整数值 Dbl=n — 已知常量浮点值 {S,C}Str=s — 已知常量字符串值 Obj{<}=c — 已知类类型(精确或子类) Arr(T1,T2,...) — 数组形状已知(packed array) Arr([T1:T2]) — 数组键值类型已知 ``` **关键创新:** - `counted` / `uncounted` 维度:字符串和数组区分是否引用计数,允许编译器针对不可数(static/常量)值做更激进优化 - `empty` / `non-empty` 维度:数组区分空/非空,消除冗余的 empty 检查 - Monotonic lattice:类型只能向一个方向变化(Index 中的类型只能收缩,分析中的类型只能增长) **AOT 借鉴优先级: P0** 当前 AOT 使用 `TYPE_INT`, `TYPE_STRING`, `TYPE_ARRAY` 等离散常量,没有联合类型、不可数标记等细粒度信息。可以: 1. 用 bitset 表示联合类型(`int|string` 而不是 `mixed`) 2. 添加 `Immutable` 标记(编译期可证明不可变的值) 3. 添加 `NonEmpty` 标记(编译期可证明非空的数组) 4. 构建类型格(lattice),定义 meet(⊓)和 join(⊔)操作 ```php // 当前 public const TYPE_INT = 1; // 建议:bitset class Type { const BINT = 1 << 0; const BSTRING = 1 << 1; const BBOOL = 1 << 2; const BFLOAT = 1 << 3; // ... const BIMMUTABLE = 1 << 16; // 不可变标记 const BNONEMPTY = 1 << 17; // 非空标记 // union type: int|string = BINT | BSTRING // meet: narrower = more specific // join: wider = more general } ``` --- ### 2.3 抽象解释器(Abstract Interpreter) **文件**: `hphp/hhbbc/interp.cpp` (6,685 行), `interp.h` HHBBC 使用**抽象解释器**进行函数级的类型推导。这与传统的 SSA + constraint solving 不同: ``` 算法(analyze_func): 1. 初始化 entry block 的输入状态(参数类型 = Index 提供的保守假设) 2. Work list ← entry blocks 3. While work list not empty: a. 取出一个 block b. 在 block 上逐指令运行抽象解释器 c. 遇到可能抛异常的指令 → 传播当前状态到异常边 d. 遇到分支指令 → 传播分支后状态到 taken 边 e. Block 可能 fallthrough → 传播最终状态到 fallthrough 边 f. 如果目标 block 的输入状态改变了 → 加入 work list ``` 抽象解释器的关键特性: - **状态传播**: 每条指令后计算新的 `State`(locals + eval stack 类型信息) - **Factored CFG**: 异常边被单独建模(`FactoredExitBlock`),普通指令不会因为 "可能抛异常" 而打断基本块 - **类型特化**: 分支条件自动收窄类型(如 `if (is_int($x))` → then 分支中 `$x` 收窄为 `Int`) - **常量传播**: 内置常量折叠和值跟踪 **AOT 借鉴优先级: P1** 当前 AOT 的 `SsaTypeOptimizer` 做了一定程度的类型收窄,但: - 没有 factored CFG 概念(异常不参与数据流) - 没有抽象解释器框架(每条指令定义 `step(state) → new_state`) - 没有迭代数据流分析 可以在 SSA 之上实现轻量级的抽象解释: ```php class AbstractInterpreter { // 对每个 SSA 基本块运行抽象解释 function analyzeBlock(Block $block, State $inputState): State { foreach ($block->instructions as $instr) { $inputState = $this->step($instr, $inputState); } return $inputState; } // 每条指令定义如何变换状态 function step(Instruction $instr, State $state): State { // switch on instruction type // return new State with updated types } } ``` --- ### 2.4 类型感知死代码消除(Type-Aware DCE) **文件**: `hphp/hhbbc/dce.cpp` (3,102 行) HHBBC 的 DCE 不同于传统的 liveness-based DCE——它**结合类型分析**来发现更多死代码: ``` 两种 DCE: 1. Local DCE — 单个基本块内 - 向后遍历 block - 维护 "反向栈":标记哪些 eval stack slot 未来会被使用 - 未被使用的栈 slot → 产生该 slot 的指令可被删除 - 未被使用的 local store → 消除 2. Global DCE — 跨基本块 - 对 locals 做 liveness 分析 - 允许跨 block 消除死 store ``` 关键设计:**类型感知**—DCE 需要知道每条指令的类型才能正确判断。例如: - 如果 `$x` 在某条路径上类型为 `Bottom`(不可达),使用 `$x` 的代码可能是 dead code - 如果 `$x` 是 counted 类型,相关 inc/dec ref 操作不能被消除 **AOT 借鉴优先级: P2** 当前 AOT 的 DCE 基本依赖 GCC/Clang 的 `-O2`。可以增加编译期的 local DCE: - 消除对未使用局部变量的赋值 - 消除无副作用的纯计算(如果结果未被使用) - 利用类型信息判断操作是否可能有副作用 --- ### 2.5 DataType 编码:3-of-7 纠错码 **文件**: `hphp/runtime/base/datatype.h` HHVM 的运行时类型标签使用极巧妙的位编码: ```cpp // DataType 是 uint8_t // - bit 0 (LSB): countedness — 0 = 确定不可数 // - bits 1-7: 3-of-7 纠错码 — 恰好 3 个 bit 为 1 // 类型检测变为简单的位操作: // 检查是否是 Vec 或 Dict: dt <= KindOfVec // 检查是否有 persistent 版本: dt <= KindOfString // 检查是否是 null/uninit: dt >= KindOfUninit ``` 3-of-7 编码的特性: - 恰好 3 个 bit 为 1 的 8-bit 值共 C(7,3) = 35 个(每对 persistent/counted 共用同一个 3-of-7 码) - 任一类型的检测:`(dt & type_mask) == type_tag` 两指令完成 - unsigned LT/GT 比较实现高效类型分组检测 **AOT 借鉴优先级: P4** 这对 AOT 编译器的**生成代码质量**有启发——但主要在 phpx 层。可以考虑给 phpx 的 `Variant` 类型标签使用更高效的编码,优化 `is_int()`/`is_string()` 等运行时类型检查。 --- ### 2.6 RepoAuthType:字节码空间高效类型存储 **文件**: `hphp/runtime/base/repo-auth-type.h`, `repo-auth-type-tags.h` HHBBC 将分析得到的类型信息编码为 `RepoAuthType`,嵌入字节码流(`AssertRAT` 指令),供 JIT 使用。 设计要点: - 紧凑编码(`CompactTaggedPtr`):类型 tag + 可选指针(类名/数组形状)打包在一个指针宽度 - 覆盖从 `Uninit`(最精确)到 `Cell`(最宽泛)的完整格 - `SubObj` / `SubCls` 标签支持子类关系 - 数组形状特殊化(packed array 的精确类型) **AOT 借鉴优先级: P3** 当前 AOT 的类型标注通过 C++ 类型系统(`int64_t`, `php::string`, `php::array`)表示。对于 typed property,可以利用类似 RAT 的思想生成更精确的 C++ 类型声明。 --- ### 2.7 Index 依赖追踪 **文件**: `hphp/hhbbc/index.cpp` (30,132 行) Index 不仅是类型信息的存储,更核心的是**依赖追踪机制**: ``` 依赖类型(DependencyKind): - ReturnTy — 函数返回类型 - ConstVal — 常量值 - ClsConst — 类常量 - PropType — 属性类型 - PublicSProp — 公共静态属性类型(特别重要!) ``` 当 Index 中某个函数的返回类型更新时,所有查询过该返回类型的函数都会被标记为需要重新分析。 **公共静态属性特殊处理**: 公共静态属性可以被任何函数修改,因此 Index 追踪所有的 mutation 操作。当分析发现某静态属性从未被修改时,可以做更激进的常量传播。 **AOT 借鉴优先级: P1** 与 2.1 的全程序分析配套——Index 依赖追踪是实现迭代分析的基础。在 AOT 中: ```php class Index { // 返回类型:funcName => Type // 依赖:funcName => [depends_on_funcName, ...] // 脏标记:funcName => bool(需要重新分析) } ``` Preprocessor 扫描所有文件后,构建初始 Index,然后迭代分析直到收敛。 --- ### 2.8 Factored CFG(异常因子化的控制流图) **文件**: `hphp/hhbbc/cfg.h`, `parse.cpp` HHBBC 的控制流图将异常边**因子化**:不在每个可能抛异常的指令处终止基本块,而是让基本块尽可能大。 ``` 传统 CFG: instr1 ; block 1 instr2(may_throw) ; block 1 在此终止(因为可能抛异常) --- instr3 ; block 2 Factored CFG: instr1 ; block 1 instr2(may_throw) instr3 ; block 1 包含多条指令 ; 异常边从 factored exit edge 连接到异常 handler ``` 好处: - 更大的基本块 → 数据流分析更高效(更少的 block 边界) - 类型信息可以排除异常可能性 → 在优化阶段可以删除异常边 - JIT 阶段更容易做指令调度 **AOT 借鉴优先级: P4** 对于 AOT 编译器(翻译到 C++ 而非直接生成机器码),CFG 主要由 GCC 处理。但在函数着色(function coloring)和 SSA 分析中,更精确的异常建模可以参考。 --- ### 2.9 并行分析(Parallel Analysis) **文件**: `hphp/hhbbc/parallel.cpp` (69 行), `README` HHBBC 的不动点迭代中,每个 work unit 的分析可以**完全并行**: ``` 线程安全模型: - 分析阶段:只能读 Index(内部线程安全)+ 读 php 元数据(不可变) - 合并阶段:单线程更新 Index(不需要锁) ``` 这利用了 "Index 信息永远不会错误" 的特性——即使两个线程基于不同版本的 Index 进行分析,合并结果也不会产生错误信息。 **AOT 借鉴优先级: P3** 与 KPHP 的流水线并行类似。在 AOT 中,可以并行分析独立函数/类。需要注意的是 Index 合并必须串行(或使用 lock-free 结构)。 --- ### 2.10 Public Static Property 优化 **文件**: `hphp/hhbbc/index.cpp` HHBBC 追踪每个公共静态属性在整个程序中的 mutation: ``` - 初始状态:保守假设(可能被任何函数修改) - 每轮分析中,记录哪些函数修改了哪些 static prop - 如果一个 static prop 在分析中从未被修改 → 可以常量折叠 - 如果一个 static prop 只被赋值一种类型 → 类型可以收窄 ``` 这比简单的 "是否有写入" 分析更精确,因为它是**全程序**分析,能看到跨文件的 mutation。 **AOT 借鉴优先级: P2** 当前 AOT 中,公共静态属性总是使用 `Variant`(mixed 类型)。通过全程序分析,可以将只在内部赋值的静态属性优化为精确 C++ 类型。 --- ## 三、工具链分析 ### 3.1 测试基础设施 HHVM 有庞大的测试体系: | 层级 | 目录 | 数量 | 用途 | |------|------|------|------| | Quick test | `hphp/test/quick/` | 865 个 .php | 快速回归测试 | | Slow test | `hphp/test/slow/` | 7,927 个 .php | 全面功能/性能测试 | | Zend test | `hphp/test/zend/` | ~4,500 个 | PHP 兼容性测试(来自 php-src) | | Ext test | `hphp/test/ext/` | ~800 个 | 扩展功能测试 | | Server test | `hphp/test/server/` | ~100 个 | HTTP/RPC 集成测试 | | HHBBC unit test | `hphp/hhbbc/test/` | 3 个 C++ 文件 | 编译器内部测试 | | **总计** | | **~14,675 个** | | 测试运行器特点: - Quick vs Slow 分层:Quick(~1 秒运行)用于 pre-commit,Slow(~10 分钟)用于 CI - 支持多种运行模式:interp / JIT / hhbbc + JIT / RepoAuthoritative - Zend 测试:直接复用 php-src 官方测试,验证 PHP 兼容性 **AOT 借鉴:** - Quick/Slow 分层测试策略——将现有 `tests/compiler/` 按运行时间分类 - 直接复用 php-src 官方 PHPT 测试——验证 AOT 编译器的 PHP 行为兼容性 - HHBBC 内部单元测试太少(仅 3 个),不应效仿——AOT 的 PHPUnit 测试覆盖更好 ### 3.2 Hack 类型检查器 HHVM 的 Hack 语言有完整的**静态类型检查器**(`hphp/hack/`),与编译器独立运行: - 编译期类型标注(`int`, `string`, `vec`, `dict`, `shape(...)`) - 渐进类型(gradual typing):可以从无标注逐步迁移 - IDE 集成(LSP 协议支持) - 类型覆盖率追踪 **AOT 借鉴:** - 当前 AOT 使用 `@phpstan-type` 等注解,可以集成 phpstan 做编译前的类型检查 - 类型覆盖率是一个有用的度量:X% 的函数/变量有精确的类型标注 ### 3.3 Tracing / Debug 基础设施 HHVM 有丰富的调试和追踪: - `TRACE_SET_MOD(hhbbc)` — 模块级条件日志(编译期开关) - `hphp/tools/` — 多种分析工具(字节码查看器、profiler 等) - `debug.cpp` — 类型/状态的可读化打印 **AOT 借鉴:** - 当前 `-vv` 详细输出可以借鉴 TRACE_MODULE 方式,按模块过滤日志 - 类型分析结果的可视化可以辅助调试优化器 ### 3.4 RepoAuthoritative 模式 HHVM 支持**编译一次,部署多次**的模式: ``` 源码 → HHBBC 全程序分析 → 优化后字节码 Repo → 多进程直接加载 Repo ``` - Repo 中存储**预分析的类型信息**(RepoAuthType) - 运行时不需要再次做类型推断 - 字节码级别的 interning (string/class/function id) - 多进程共享同一个 Repo(mmap) **AOT 借鉴: P4** 当前 AOT 直接生成 `.cc` 文件并编译为二进制,已经在 "编译一次,部署多次" 的路径上。但 Repo 的 mmap 共享思想可以用于多进程环境下的常量池共享(而非每个进程独立加载)。 --- ## 四、类型系统兼容性分析 ### 4.1 Hack vs PHP vs AOT Compiler | 特性 | PHP 8.2 | Hack | AOT Compiler | |------|---------|------|-------------| | 基础类型 | mixed, int, string, float, bool, array, null, void, never | int, string, float, bool, null, void, noreturn, mixed, dynamic, nonnull, nothing | 同 PHP | | 联合类型 | `int\|string` | `int\|string` (但实践中不鼓励) | 支持 | | 交叉类型 | `X&Y` (8.1+) | 通过 `where` 约束 | 不支持 | | 泛型 | 无 | `vec`, `dict`, `class Box` | `std::vector` (C++ 原生) | | 数组类型 | `array` | `vec`, `dict`, `keyset` | `php::array` | | Shapes | 无 | `shape('x' => int, 'y' => string)` | 无 (建议用 object) | | 枚举 | enum (8.1) | enum, enum class (带 label) | 支持 PHP 8.1 enum | | 可空类型 | `?int` | `?int` (仅函数参数) | 支持 | | nothing / bottom | 无 | 有 (空函数返回, unreachable) | 无 | | dynamic | 无 | 有 (选择性放弃类型检查) | 无 | ### 4.2 类型格的关键差异 Hack/HHBBC 的类型系统比 PHP 丰富得多: ``` HHBBC 类型格: Cell (any value) | InitCell (not Uninit) | | Prim Boxed types (Obj, Res, ...) | InitPrim (not null) | | | Num Bool Str/ArrKey | | Int Dbl 底部: Bottom (无值 - unreachable) 顶部: Cell (任何值) ``` 特性: - **Bottom**: 表示不可达路径的类型,可以消除 dead code - **Counted/Uncounted**: 静态字符串 vs 运行时分配字符串,不同生命周期 - **Array shapes**: `dict<'name' => string, 'age' => int>` 是独立类型 - **Wait handle**: `WaitH` 表示异步结果的类型 ### 4.3 Hack 的渐进类型设计 Hack 从 PHP 演进而来,支持渐进迁移: - `mixed` — 任何类型(等同于无标注 PHP) - `dynamic` — 任何类型,且不报类型错误(更宽松的 mixed) - `<<__Soft>>` — 软类型提示(运行时不强制检查) 这种设计允许大型代码库逐步添加类型标注,避免 "全有或全无"。 ### 4.4 与 AOT 编译器的语法差异 | 特性 | HHBBC 输入 (HHAS) | AOT Compiler | |------|------|-------------| | 基础 PHP 版本 | Hack (PHP 5.6 分支) | PHP 8.2+ | | 类型标注 | 强制(Hack 语言要求) | 可选 (phpstan 注解) | | 泛型 | `vec`, `dict`, 自定义泛型类 | 无原生泛型 | | Lambda / 闭包 | `$x ==> $x + 1` (short lambda) | PHP 闭包 (`function($x) { return $x + 1; }`) | | async / await | 原生支持 (WaitHandle) | 无 | | Shapes | `shape('x' => int)` | 无 | | Enum class | 支持(带 label 的 enum class) | 仅 PHP 8.1 enum | | XHP | HTML 模板语法 | 无 | | Case types | `case type T = int \| string` | 无 | --- ## 五、总结与优先级建议 | 优先级 | 技术 | 难度 | 收益 | 说明 | |--------|------|------|------|------| | **P0** | 全程序不动点分析 | 极高 | 极高 | 跨文件类型推导、返回类型收窄、消除伪动态调用 | | **P0** | Trep 类型格 | 中 | 极高 | 精确联合类型、不可数标记、非空标记,提升所有优化的精度 | | **P1** | 抽象解释器 | 高 | 高 | 替代/增强当前 SSA 分析,支持分支类型收窄、常量传播 | | **P1** | Index 依赖追踪 | 高 | 高 | 全程序分析的基础,增量编译 | | **P2** | 类型感知 DCE | 中 | 中 | 消除更多死代码,减小生成 C++ 代码体积 | | **P2** | Public Static Prop 优化 | 中 | 中 | 全局静态属性类型精确化 | | **P3** | RepoAuthType 存储 | 低 | 中 | 产物中嵌入精确类型信息(对 C++ 生成阶段意义有限) | | **P3** | 并行分析 | 高 | 中 | 大型项目编译提速 | | **P4** | DataType 编码优化 | 低 | 低 | 对 phpx Variant 类型检查微优化 | | **P4** | Factored CFG | 高 | 低 | C++ 编译器已处理 CFG 优化 | | **P5** | Repo mmap 共享 | 高 | 低 | 特定部署场景优化 | ### 关键认识 1. **HHBBC 的全程序分析是其最大的差异化优势**——AOT 编译器最大的架构差距就在于此。单文件分析无法看到跨文件的类型信息,导致很多调用被迫使用动态分发。 2. **类型系统是优化的核心引擎**——HHBBC 的 trep + specialization + monotonic lattice 投入巨大(约 12k 行),但这正是所有优化 pass 的质量基础。 3. **依赖追踪是实现增量分析的关键**——Index 自动记录的 "谁查询了什么" 信息,使得只需重新分析受影响的函数,避免全程序每次重新分析。 4. **factored CFG 和 DataType 编码更偏 JIT 场景**——这些设计为运行时 JIT 优化,AOT 编译器生成 C++ 源码,由 GCC/Clang 处理这些底层优化,不应重复投资。 5. **测试基础设施值得学习**——Quick/Slow 分层、直接复用 php-src 测试、多运行模式,对 AOT 编译器的测试 CI 建设有直接参考价值。