27 KiB
PHP Zend Optimizer / OPcache / JIT 设计分析:可引入 AOT 编译器的机制
本文档分析了 php-src (v8.4.14) 中 Zend Optimizer、OPcache 和 JIT 的实现,识别出可引入 AOT 编译器的设计模式、算法和数据流框架。
源码位置:
- 优化器:
~/soft/php/php-8.4.14/Zend/Optimizer/ - OPcache:
~/soft/php/php-8.4.14/ext/opcache/ - JIT:
~/soft/php/php-8.4.14/ext/opcache/jit/
概览
PHP 的优化器是本领域最成熟的动态语言高端优化器之一。它包含完整的 SSA (Static Single Assignment) 构建、e-SSA (Extended SSA) 类型/范围推断、SCCP (Sparse Conditional Constant Propagation)、逃逸分析、死代码消除、调用图分析、profile-guided tracing JIT 等完整编译器优化基础设施。
AOT 编译器可以从以下方面借鉴:
| 优先级 | 设计/模块 | 实施量级 | 收益 |
|---|---|---|---|
| P1 | Pass Pipeline 架构 | ~100 行框架 | 架构清晰、可插拔、分 O0/O1/O2 |
| P1 | SSA + e-SSA 构建 | 中 | 所有高级优化的基础 |
| P1 | 类型推断 (Type & Range Inference) | 中 | 精确的类型推导能力 |
| P2 | SCDF 通用数据流框架 | ~300 行 | 可复用于 SCCP/类型推断/优化 |
| P2 | SCCP 常量传播 | 中 | 条件常量折叠 + 不可达代码消除 |
| P2 | DCE 死代码消除 | ~400 行 | worklist 驱动的精确 DCE |
| P3 | 逃逸分析 (Escape Analysis) | ~500 行 | 栈分配、消除引用计数 |
| P3 | 调用图 (Call Graph) | ~400 行 | 跨函数分析、内联决策、死代码 |
| P4 | JIT IR 框架 | 极重 | 参考其 IR 设计的抽象层次 |
| P4 | OPcache 持久化/File Cache | 轻 | 序列化优化后的 IR 缓存 |
1. Pass Pipeline 架构
设计
PHP 定义了 16 个优化 Pass,每个 Pass 对应一个 bitmask 位(zend_optimizer.h:28-46):
#define ZEND_OPTIMIZER_PASS_1 (1<<0) // 简单局部优化 (常量替换/折叠)
#define ZEND_OPTIMIZER_PASS_2 (1<<1) //
#define ZEND_OPTIMIZER_PASS_3 (1<<2) // Jump 优化
#define ZEND_OPTIMIZER_PASS_4 (1<<3) // INIT_FCALL_BY_NAME -> DO_FCALL
#define ZEND_OPTIMIZER_PASS_5 (1<<4) // CFG 优化 (block pass)
#define ZEND_OPTIMIZER_PASS_6 (1<<5) // DFA 优化 (type/range inference → 单函数)
#define ZEND_OPTIMIZER_PASS_7 (1<<6) // CALL GRAPH 优化 (跨函数分析)
#define ZEND_OPTIMIZER_PASS_8 (1<<7) // SCCP (常量传播)
#define ZEND_OPTIMIZER_PASS_9 (1<<8) // 临时变量优化
#define ZEND_OPTIMIZER_PASS_10 (1<<9) // NOP 移除
#define ZEND_OPTIMIZER_PASS_11 (1<<10) // 合并相同常量
#define ZEND_OPTIMIZER_PASS_12 (1<<11) // 调整栈使用
#define ZEND_OPTIMIZER_PASS_13 (1<<12) // 移除未使用变量
#define ZEND_OPTIMIZER_PASS_14 (1<<13) // DCE (死代码消除)
#define ZEND_OPTIMIZER_PASS_15 (1<<14) // 收集常量 (unsafe)
#define ZEND_OPTIMIZER_PASS_16 (1<<15) // 函数内联
流水线调度
zend_optimize() 函数 (zend_optimizer.c:1067-1183) 按顺序执行各个 pass,每个 pass 只处理已执行 pass 产生的结果:
pass1 (常量折叠) → pass3 (跳转优化) → pass4 (函数调用优化)
→ pass5 (CFG) → pass6 (DFA + 类型推断) → pass9 (临时变量)
→ pass10 (NOP移除) → pass11 (常量合并) → pass13 (变量清理) → ...
当 PASS_6 + PASS_7 同时启用时,zend_optimize_script() 走更复杂的调用图路径:
build_call_graph → zend_optimize (per-func) → analyze_call_graph
→ 构建 call_map → dfa_analyze_op_array (per-func)
→ dfa_optimize_op_array (per-func, with call context)
→ pass9 → pass11 → pass13 → pass12 (stack adjust) → redo_pass_two
关键设计点
1. Bitmask 开关 + 注册式 Pass
用户可以按 bitmask 组合任意 pass。同时支持 zend_optimizer_register_pass() 注册外部 pass(如 JIT 自己的优化 pass):
static struct {
zend_optimizer_pass_t pass[ZEND_OPTIMIZER_MAX_REGISTERED_PASSES];
int last;
} zend_optimizer_registered_passes;
注册的 pass 在所有内置 pass 之后执行(zend_optimizer_call_registered_passes)。
2. 每个 pass 可选 dump 输出
通过 debug_level 控制,支持在任意 pass 前后输出中间表示,用于调试和性能分析。
3. Per-function 和 script-level 双层优化
zend_optimize(op_array, ctx)— 单函数优化,保守(不利用跨函数信息)zend_optimize_script(script, ...)— 全脚本优化 with call graph,可做跨函数优化
AOT 借鉴
AOT 编译器可以定义类似的 pass pipeline:
enum AotPass: int {
case CONSTANT_FOLD = 1 << 0;
case TYPE_CHECK_INSERT = 1 << 1;
case ESCAPE_ANALYSIS = 1 << 2;
case DEVIRTUALIZE = 1 << 3;
case DEAD_CODE_ELIM = 1 << 4;
case FUNCTION_INLINE = 1 << 5;
case LOOP_OPTIMIZE = 1 << 6;
case BOX_ALLOC_ELIM = 1 << 7;
}
按优化级别组合:
const O0 = AotPass::TYPE_CHECK_INSERT->value; // 必需的基础代码生成
const O1 = O0 | AotPass::CONSTANT_FOLD->value; // 基础优化
const O2 = O1 | AotPass::DEVIRTUALIZE->value | AotPass::FUNCTION_INLINE->value;
2. SSA (Static Single Assignment) + e-SSA
数据结构
SSA 构建在控制流图 (CFG) 之上:
CFG (zend_cfg.h:84-92):
typedef struct _zend_cfg {
int blocks_count; // 基本块数量
int edges_count; // 边数量
zend_basic_block *blocks; // 基本块数组
int *predecessors; // 前驱列表
uint32_t *map; // opnum → block 映射
uint32_t flags;
} zend_cfg;
typedef struct _zend_basic_block {
int *successors; // 后继块索引
uint32_t flags;
uint32_t start; // 起始 opcode
uint32_t len; // opcode 数
int successors_count;
int predecessors_count;
int idom; // 立即支配者
int loop_header; // 最近循环头
int level; // 支配树深度
int children; // 支配的子块链表
} zend_basic_block;
SSA (zend_ssa.h:135-143):
typedef struct _zend_ssa {
zend_cfg cfg; // 控制流图
int vars_count; // SSA 变量数
int sccs; // 强连通分量数
zend_ssa_block *blocks; // 每基本块的 φ 函数
zend_ssa_op *ops; // 每指令的 use-def 信息
zend_ssa_var *vars; // 每个 SSA 变量的 def-use 链
zend_ssa_var_info *var_info; // 类型推断结果 (类型位掩码 + 范围)
} zend_ssa;
SSA Op (zend_ssa.h:82-92):
typedef struct _zend_ssa_op {
int op1_use;
int op2_use;
int result_use;
int op1_def; // 这个指令定义的 SSA 变量
int op2_def;
int result_def;
int op1_use_chain; // use-def 链
int op2_use_chain;
int res_use_chain;
} zend_ssa_op;
e-SSA: Extended SSA with Pi Nodes
这是 PHP 优化器最精妙的设计之一。Pi node 是一种特殊的 φ 函数,用于表示从条件分支推断出的类型/范围约束。
Pi 约束 (zend_ssa.h:42-59):
typedef struct _zend_ssa_range_constraint {
zend_ssa_range range; // 范围约束 [min, max]
int min_var; // 符号范围下界变量
int max_var; // 符号范围上界变量
zend_ssa_negative_lat negative; // 否定潜力
} zend_ssa_range_constraint;
typedef struct _zend_ssa_type_constraint {
uint32_t type_mask; // 类型掩码 (与操作后得到的窄化类型)
zend_class_entry *ce; // 类条目 (for instanceof)
} zend_ssa_type_constraint;
typedef union _zend_ssa_pi_constraint {
zend_ssa_range_constraint range;
zend_ssa_type_constraint type;
} zend_ssa_pi_constraint;
工作原理: 对于 if ($x > 0) 这样的条件:
- 在 truthy 分支,插入
Pi($x, range[1, LONG_MAX])— 将$x的 SSA 变量限定在 > 0 的范围 - 在 falsy 分支,插入
Pi($x, range[LONG_MIN, 0])— 将$x限定在 ≤ 0
这允许分支内使用精化的类型/范围信息进行后续优化,而不改变原变量的显式赋值链。
SSA 构建流程
1. zend_build_cfg() → 构建控制流图 (含支配树、循环检测)
2. zend_build_dfg() → 构建数据流图 (计算 use/def sets)
3. zend_build_ssa() → 放置 φ 函数 → 重命名变量 → 构建 SSA 形式
4. zend_ssa_compute_use_def_chains() → 连接 use-def 链
5. zend_ssa_find_sccs() → 找强连通分量 (用于类型推断)
6. zend_ssa_inference() → 类型推断 + 范围推断 (填充 var_info)
AOT 借鉴
AOT 编译器不需要 SSA 形式(因为生成的是 C++ 代码,不是直接操作寄存器),但 e-SSA 的以下概念可以直接用:
-
Pi 约束的概念: 在条件分支中插入类型窄化标记,使分支内变量具有更精确的类型。这直接对应前文 TypeSpecifier / Type Narrowing (#7) 的实现基础。
-
Type & Range 信息关联到每个表达式: 类似 SSA
var_info的设计,可以在 AOT 的 FunctionContext 中为每个变量/表达式维护{type_mask, range, ce}三元组。 -
Use-def 链用于优化判断: 当需要判断一个变量是否有且仅有一个
use时,SSA 的 use_chain 提供了 O(1) 查询。
3. 类型推断 (Type & Range Inference)
类型系统
PHP 使用位掩码表示类型信息(zend_type_info.h 定义),这是最具特色的设计:
#define MAY_BE_UNDEF (1<< 0)
#define MAY_BE_NULL (1<< 1)
#define MAY_BE_FALSE (1<< 2)
#define MAY_BE_TRUE (1<< 3)
#define MAY_BE_LONG (1<< 4)
#define MAY_BE_DOUBLE (1<< 5)
#define MAY_BE_STRING (1<< 6)
#define MAY_BE_ARRAY (1<< 7)
#define MAY_BE_OBJECT (1<< 8)
#define MAY_BE_RESOURCE (1<< 9)
#define MAY_BE_REFERENCE (1<<10)
#define MAY_BE_CALLABLE (1<<11)
#define MAY_BE_ITERABLE (1<<12)
#define MAY_BE_VOID (1<<13)
#define MAY_BE_INDIRECT (1<<14)
// 方便组合
#define MAY_BE_ANY (MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|...)
#define MAY_BE_TRUTHY (MAY_BE_TRUE|MAY_BE_LONG|... /* 非 0/''/[]/null */)
#define MAY_BE_FALSEY (MAY_BE_UNDEF|MAY_BE_NULL|MAY_BE_FALSE|...)
核心优势: 位运算极快。类型运算(合并/交集/差集)仅需一条 AND/OR/NOT 指令:
// 合并两个变量的类型
uint32_t result_type = info1 | info2;
// 检查是否可能为 string
if (info & MAY_BE_STRING) { ... }
// 交集
uint32_t common = info1 & info2;
范围推断
每个 SSA 变量附带 zend_ssa_range { min, max, underflow, overflow }:
typedef struct _zend_ssa_range {
zend_long min;
zend_long max;
bool underflow; // 是否有下溢风险
bool overflow; // 是否有上溢风险
} zend_ssa_range;
核心算法(zend_inference.c:1071)基于 V. Campos 的 "Speed and Precision in Range Analysis, SBLP'12" 论文:
- Warmup 阶段 (16 pass): 在 SCC(强连通分量)上传播范围,使用 widening 加速收敛
- Narrowing 阶段: 将范围逐渐收窄,消除 widening 造成的过度近似
- Zend Engine 专门的算术语义:
zend_add_will_overflow(),zend_sub_will_overflow()等精确检测整数溢出
运算符范围推断示例:
// ADD: 结果范围
min = OP1_MIN() + OP2_MIN()
max = OP1_MAX() + OP2_MAX()
overflow = OP1_RANGE_OVERFLOW() || OP2_RANGE_OVERFLOW()
|| zend_add_will_overflow(OP1_MAX(), OP2_MAX())
// 结果类型: 如果 overflow 为真,类型增加 MAY_BE_DOUBLE
// (PHP int 溢出自定转为 float)
每个 Opcode 的 update_type_info
_zend_update_type_info() 是一个巨大的 switch,针对每个 Zend opcode 精确计算结果类型和范围。例如 ZEND_ASSIGN_DIM(数组赋值),不仅更新被赋值元素的类型,还更新数组整体的类型,考虑 MAY_BE_PACKED_GUARD(打包数组守卫)和引用计数推断。
AOT 借鉴
-
位掩码类型系统: 是最适合 AOT 编译器的轻量类型表示方案。AOT 当前用字符串类型(
TYPE_INT = 'int'),无法高效地进行"可能是 int 或 string"这种复合表示。位掩码提供了 O(1) 的 union/intersect/test 操作。 -
范围推断: 可为 C++ 代码生成选择最优的整数类型(
int32_tvsint64_tvsBigInt),避免不必要的 BigInt 分配。 -
Overflow 追踪: 精确判定何时需要从 int64 转为 float/BigInt,只在真正可能溢出时才插入转换代码。
-
每个 opcode 的类型更新表:
_zend_update_type_info()的设计可以直接映射到 AOT 的 Rule 系统中——每个 opcode 对应一个 Rule,负责输出该操作的结果类型。
4. SCCP (Sparse Conditional Constant Propagation)
核心设计
SCCP 同时做常量传播和条件常量折叠,还可以消除不可达代码(不需要额外的死代码消除 pass)。
实现在 sccp.c,基于 scdf.h(SCDF 框架)。
值格 (Value Lattice)
TOP (未定义)
/ | \
C1 C2 C3 (常量值)
\ | /
BOT (过定义 = 非常量)
- TOP: 还不知道这个变量的值(乐观假设)
- BOT: 已知这个变量不是常量
- 常量值: 已知精确值
算法关键点 (来自 sccp.c:30-74 的注释)
meet 操作 (φ 函数的合并):
- BOT + any = BOT
- TOP + any = any
- C_i + C_i = C_i (两个相同常量)
- C_i + C_j = BOT (两个不同常量)
指令求值:
- 任何操作数是 BOT → 结果是 BOT (例外: ASSIGN 的 op1)
- 永远不能求值的指令 → BOT
- 任何操作数是 TOP → 结果是 TOP
- 所有操作数都是已知常量 → 尝试编译时求值 → 成功返回常量值,失败返回 BOT
分支可行性判断:
- 分支在 BOT 上 → 所有后继可行
- 分支在 TOP 上 → 都没有后继不可行(等待更多信息)
- 分支在已知常量上 → 只有满足条件的分支可行
SCDF 框架 (scdf.h)
SCCP 被构建在 SCDF (Sparse Conditional Data Flow) 框架之上,这是一个通用的稀疏条件数据流分析引擎:
typedef struct _scdf_ctx {
zend_op_array *op_array;
zend_ssa *ssa;
zend_bitset instr_worklist; // 待处理的指令
zend_bitset phi_var_worklist; // 待处理的 Phi/SSA 变量
zend_bitset block_worklist; // 待处理的块
zend_bitset executable_blocks; // 可执行块
zend_bitset feasible_edges; // 可行边
struct {
void (*visit_instr)(...); // 处理一条指令
void (*visit_phi)(...); // 处理一个 φ 函数
void (*mark_feasible_successors)(...); // 标记可行后继
} handlers;
} scdf_ctx;
使用模式: SCCP 实现了 visit_instr (常量求值)、visit_phi (meet 操作)、mark_feasible_successors (分支可行性)。类型推断也用类似的 worklist 传播算法。
通用 worklist 机制:
// 当一个变量的值改变时,将它的所有 use 加入 worklist
static inline void scdf_add_to_worklist(scdf_ctx *scdf, int var_num) {
const zend_ssa_var *var = &ssa->vars[var_num];
int use;
FOREACH_USE(var, use) {
zend_bitset_incl(scdf->instr_worklist, use); // 标记使用该变量的指令
}
FOREACH_PHI_USE(var, phi) {
zend_bitset_incl(scdf->phi_var_worklist, phi->ssa_var);
}
}
AOT 借鉴
-
SCDF 框架是最直接可借鉴的:约 300 行 C 代码,提供了一个通用的 worklist 驱动的条件数据流引擎。AOT 可以移植为 PHP 类,复用于 SCCP、类型推断、逃逸分析等多项优化 pass。
-
TOP/BOT 格模型: AOT 编译器在分析类型时可以使用相同的格结构:
- TOP = 未知类型 (分析初期)
- BOT = 矛盾类型 (发现了不一致)
- 具体值 (常量或确切类型)
-
条件分支可行性: SCCP 的分支可行性判断可以直接帮助 AOT 在编译时消除不可达分支,生成更简化的 C++ 代码。
5. DCE (Dead Code Elimination)
算法 (dce.c)
PHP 的 DCE 采用乐观策略:
1. 假设所有指令和 φ 函数都是 dead
2. 标记所有有明显副作用的指令为 live (side-effect instruction)
3. 从 live 指令出发,标记其操作数的定义指令为 live (use-def 链反向传播)
4. 重复直到 worklist 为空
5. 删除所有仍标记为 dead 的指令
关键判断 may_have_side_effects() (dce.c:74-100):
将 Zend opcode 分为三种:
- 永远无副作用(如 ADD、CONCAT、BOOL_NOT):可以被 DCE 消除
- 可能产生 notice 但无本质副作用(如 DIV_BY_ZERO 触发 warning):可配置是否消除
- 永远有副作用(如 ECHO、THROW、ASSIGN_OBJ):必须保留
特别能力: 可以消除"非逃逸数组/对象的冗余修改"以及"无用的数组/对象分配"。如果一个数组只在局部被构建、修改和使用,中间步骤的 ASSIGN_DIM 可能被消除。
AOT 借鉴
AOT 编译器的 DCE 可以更激进(因为编译时已知类型):
- 副作用分类矩阵: 为 AOT 的表达式/语句类型建立副作用表格,精确标记哪些操作必须保留
- 逃逸感知 DCE: 结合逃逸分析,消除未逃逸对象的操作——这是 AOT 最大的优化机会之一
- Control-dependence based DCE: PHP 明确表示当前的 DCE 没有考虑控制依赖(
dce.c:35-39的注释),AOT 可以做更精确的控制依赖 DCE
6. 逃逸分析 (Escape Analysis)
算法 (escape_analysis.c)
基于 Kotzmann & Mossenbock (PPPJ'05) 的经典逃逸分析算法。
核心步骤:
-
构建等价逃逸集 (
zend_build_equi_escape_sets): 使用 Union-Find 算法。如果两个 SSA 变量通过 φ 函数或 ASSIGN 关联(值相同),它们属于同一个等价类。 -
逃逸状态传播: 每个等价类有四种状态:
ESCAPE_STATE_UNKNOWN → 初始状态(零初始化的 C 内存) ESCAPE_STATE_NO_ESCAPE → 确定不逃逸(最终目标) ESCAPE_STATE_FUNCTION_ESCAPE → 逃逸到被调用函数(传入参数) ESCAPE_STATE_GLOBAL_ESCAPE → 全局逃逸(返回、赋值到全局变量、抛异常等) -
状态单调收敛: 状态只能从 UNKNOWN → NO_ESCAPE/FUNCTION_ESCAPE/GLOBAL_ESCAPE,从不反转。
-
应用逃逸信息:
- 不逃逸的数组可以在栈上分配(不需要堆分配)
- 不逃逸的对象可以避免引用计数操作
- 不逃逸的变量不需要分离(ZEND_SEPARATE)
前驱/后继边: 支持符号类型别名(SYMTABLE_ALIAS)和 HTTP 响应头别名(HTTP_RESPONSE_HEADER_ALIAS)。
AOT 借鉴
逃逸分析对 AOT 编译器可能价值最大:
-
Box allocation 消除: AOT 使用
Box<T>表示对象引用。逃逸分析可以确认哪些 Box 不需要堆分配,直接在栈上创建。 -
引用计数消除: 不逃逸的对象可以跳过
php::Object::Ref()/php::Object::Unref()操作。 -
Array 栈分配: 局部数组逃逸分析后可以改用栈上的
zend_array。 -
4 状态模型非常简单有效,AOT 可以直接映射:
- ESCAPE_STATE_NO_ESCAPE → 栈分配
- ESCAPE_STATE_FUNCTION_ESCAPE → 调用者决定
- ESCAPE_STATE_GLOBAL_ESCAPE → 堆分配
7. 调用图 (Call Graph)
设计 (zend_call_graph.h)
PHP 的调用图同时追踪 caller → callee 和 callee → caller 的双向关系:
struct _zend_call_info {
zend_op_array *caller_op_array; // 调用者
zend_op *caller_init_opline; // INIT_FCALL 指令
zend_op *caller_call_opline; // DO_FCALL 指令
zend_function *callee_func; // 被调用函数
zend_call_info *next_caller; // 链表: callee 的下一个 caller
zend_call_info *next_callee; // 链表: caller 的下一个 callee
bool recursive; // 递归调用
bool send_unpack; // 使用 SEND_UNPACK
bool named_args; // 命名参数
bool is_prototype; // 可能是子类重写的方法
bool is_frameless; // frameless 函数
int num_args;
zend_send_arg_info arg_info[1];
};
struct _zend_func_info {
zend_ssa ssa; // 函数自身的 SSA
zend_call_info *caller_info; // 谁调用了这个函数
zend_call_info *callee_info; // 这个函数调用了谁
zend_call_info **call_map; // 从 opnum 快速索引 call_info
zend_ssa_var_info return_info; // 推断出的返回类型
};
关键功能:
- 双向图:
caller_info和callee_info分别是不同的链表,支持向上(从 callee 找 caller)和向下(从 caller 找 callee)遍历 - call_map: 数组索引 opnum → call_info,O(1) 查找某个 opcode 位置对应的调用信息
- 返回类型传播: 被调用函数的 return_info 可以向上传播到调用者的 return_info
- 参数类型传播: 调用者的实参类型可以向下传播到被调用者的参数类型(用于更精确的函数体优化)
高级跨函数优化 (zend_optimize_script:1626-1728)
1. build_call_graph → 构建双向调用图
2. zend_optimize (per-func) → 每个函数做独立的 local optimization
3. analyze_call_graph → 推断函数信息 (递归标记、间接变量访问、func_get_args 等)
4. build_call_map → 为每个函数构建 opnum→call 的索引
5. dfa_analyze_op_array → 构建 SSA + 类型推断 (per-func)
6. dfa_optimize_op_array → 基于 SSA 做 SCCP + DCE + block pass
AOT 借鉴
AOT 编译器的前两步(prepare + convert)天然构建了完整的符号依赖图。可以在此之上增加:
- call_map 索引: 从每个 call site 快速查找 callee 的元信息(参数类型、返回类型、是否内联候选)
- 返回类型双向传播: 目前 AOT 的返回类型推导是自顶向下的;调用图允许从 callee 的已知返回类型回传给 caller
- 递归标记:
ZEND_FUNC_RECURSIVE_DIRECTLY/ZEND_FUNC_RECURSIVE_INDIRECTLY对内联策略决策至关重要
8. JIT IR 框架
设计
PHP JIT 使用的 IR(Intermediate Representation)是一个通用的 SSA 派生的低级中间表示,位于 ext/opcache/jit/ir/。
IR 的三个阶段:
| 阶段 | 文件 | 作用 |
|---|---|---|
| IR builder | ir_builder.h, zend_jit_ir.c |
从 Zend 字节码构建 IR 指令 |
| IR optimizer | ir_cfg.c, ir_fold.h, ir_gcm.c |
CFG 优化、常量折叠、全局代码移动 (GCM) |
| IR emitter | ir_emit.c, ir_emit_x86.h |
从 IR 发射 x86/ARM64 机器码 |
IR 指令示例: IR_ADD, IR_MUL, IR_LOAD, IR_STORE, IR_CALL, IR_GUARD, etc.
JIT 优化级别 (zend_jit.h:32-37):
#define ZEND_JIT_LEVEL_NONE 0 // 不启用 JIT
#define ZEND_JIT_LEVEL_MINIMAL 1 // 最小 JIT (子程序线程化)
#define ZEND_JIT_LEVEL_INLINE 2 // 选择性内联线程化
#define ZEND_JIT_LEVEL_OPT_FUNC 3 // 基于类型推断优化单个函数
#define ZEND_JIT_LEVEL_OPT_FUNCS 4 // 基于调用树优化
#define ZEND_JIT_LEVEL_OPT_SCRIPT 5 // 过程间分析
JIT 触发模式 (zend_jit.h:39-44):
#define ZEND_JIT_ON_SCRIPT_LOAD 0 // 所有函数加载时立即编译
#define ZEND_JIT_ON_FIRST_EXEC 1 // 首次执行时编译
#define ZEND_JIT_ON_PROF_REQUEST 2 // 根据 profile 数据编译最热的函数
#define ZEND_JIT_ON_HOT_COUNTERS 3 // N 次调用/循环迭代后编译
#define ZEND_JIT_ON_HOT_TRACE 5 // N 次调用后走 tracing JIT
AOT 借鉴
-
IR 作为 AST→C++ 的中间载体: AOT 当前直接从 AST 生成 C++ 代码。引入 IR 层可以:
- 在 IR 层面做优化(fold、GCM、寄存器分配模拟)
- 解耦前端(PHP AST)和后端(C++ codegen)
-
ir_fold.h的常量折叠表: IR 包含一个自动生成的折叠规则表 (gen_ir_fold_hash),定义了数百条代数简化规则。AOT 可以采用类似的"规则表" 驱动的常量折叠。 -
JIT 的 profiling 机制:
hot_loop/hot_func计数器 —— AOT 可以将 profile 数据嵌入生成的二进制,用于 PGO (Profile-Guided Optimization)。
9. OPcache 持久化 & File Cache
设计
OPcache 不只是一个缓存——它在缓存中存的是优化后的字节码。
原始 PHP 源码
→ 编译为 zend_op_array (原始字节码)
→ 经过 Zend Optimizer 所有 pass (SSA + 类型推断 + SCCP + DCE + ...)
→ 只保留优化后的 zend_op_array (丢弃 SSA 等临时 IR)
→ zend_persist() 序列化到共享内存 / 文件缓存
zend_persist_calc + zend_persist:两阶段序列化——
_calc计算所需的共享内存大小_persist进行实际序列化(所有指针调整为绝对偏移量)
文件缓存 (zend_file_cache.c): 将持久化后的脚本写入文件,允许跨进程重启复用。
AOT 借鉴
当前 AOT 编译器每次从 PHP 源码开始编译。可以借鉴 OPcache 的理念:
- 缓存优化后的 AST/类型信息: 在
convert()阶段之后,将带类型的 AST 序列化,下次编译时直接加载 - 增量编译: 只重新编译改动的文件及其依赖
_calc+_persist两阶段序列化: 先计算大小再分配内存/写入,避免 realloc 碎片
10. 其他值得注意的设计
zend_bitset
PHP 使用自己的 bitset 实现进行高效的集合操作。优化器频繁使用 bitset 来表示 worklist、live sets、def/use sets。
zend_worklist.h
通用的 worklist 迭代宏,SCCP 和类型推断都使用相同的 worklist 机制。设计为宏以便内联性能。
zend_arena
Arena 内存分配器用于所有优化器数据结构的快速分配和整体释放。一个 arena 绑定一个 zend_optimizer_ctx,所有 pass 共享同一个 arena。
Pass 间数据管理
SSA 等临时 IR 在 pass 完成后立即销毁(通过 arena free),只在最终的 zend_op_array 中保留优化后的结果。这保证了内存效率。
推荐采用顺序 (针对 AOT 编译器)
Phase 1: Pass Pipeline
└── 定义 AotPass 枚举 + Pipeline runner,可插拔 pass 架构
Phase 2: 位掩码类型系统
└── 借鉴 Zend 的 type mask 设计,替代当前的字符串类型常量
└── 直接映射为 C++ 的 uint32_t 常量
Phase 3: 类型推断规则
└── 每个 AST 节点/opcode 对应一个 update_type_info
└── 与 Rule 系统 (#4 设计) 结合实现
Phase 4: SCCP + DCE
└── 基于 SCDF 框架的常量传播 + 死代码消除
└── 可在 C++ 代码生成之前消去冗余表达式
Phase 5: 逃逸分析
└── Union-Find 等价逃逸集 + 4 状态传播
└── 用于 Box 分配消除、引用计数消除
Phase 6: 调用图跨函数优化
└── 在已有的符号依赖图上增加 call_map + 类型传播
每一层都可以独立实施并立即给现有代码生成带来收益。