TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

22 KiB

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. 迭代收敛:初始化所有函数为 "未知" → 逐轮分析 → 直至类型信息不再变化

实现建议:

// 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):

// 每个"基本类型"是一个 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(⊔)操作
// 当前
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 之上实现轻量级的抽象解释:

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 的运行时类型标签使用极巧妙的位编码:

// 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 中:

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<T>, dict<TK,TV>, 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<T>, dict<TK,TV>, class Box<T> std::vector<T> (C++ 原生)
数组类型 array vec<T>, dict<TK,TV>, keyset<T> 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<T> 表示异步结果的类型

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<T>, dict<K,V>, 自定义泛型类 无原生泛型
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 建设有直接参考价值。