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.
 
 

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 的以下概念可以直接用:

  1. Pi 约束的概念: 在条件分支中插入类型窄化标记,使分支内变量具有更精确的类型。这直接对应前文 TypeSpecifier / Type Narrowing (#7) 的实现基础。

  2. Type & Range 信息关联到每个表达式: 类似 SSA var_info 的设计,可以在 AOT 的 FunctionContext 中为每个变量/表达式维护 {type_mask, range, ce} 三元组。

  3. 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" 论文:

  1. Warmup 阶段 (16 pass): 在 SCC(强连通分量)上传播范围,使用 widening 加速收敛
  2. Narrowing 阶段: 将范围逐渐收窄,消除 widening 造成的过度近似
  3. 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 借鉴

  1. 位掩码类型系统: 是最适合 AOT 编译器的轻量类型表示方案。AOT 当前用字符串类型(TYPE_INT = 'int'),无法高效地进行"可能是 int 或 string"这种复合表示。位掩码提供了 O(1) 的 union/intersect/test 操作。

  2. 范围推断: 可为 C++ 代码生成选择最优的整数类型(int32_t vs int64_t vs BigInt),避免不必要的 BigInt 分配。

  3. Overflow 追踪: 精确判定何时需要从 int64 转为 float/BigInt,只在真正可能溢出时才插入转换代码。

  4. 每个 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 借鉴

  1. SCDF 框架是最直接可借鉴的:约 300 行 C 代码,提供了一个通用的 worklist 驱动的条件数据流引擎。AOT 可以移植为 PHP 类,复用于 SCCP、类型推断、逃逸分析等多项优化 pass。

  2. TOP/BOT 格模型: AOT 编译器在分析类型时可以使用相同的格结构:

    • TOP = 未知类型 (分析初期)
    • BOT = 矛盾类型 (发现了不一致)
    • 具体值 (常量或确切类型)
  3. 条件分支可行性: 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 可以更激进(因为编译时已知类型):

  1. 副作用分类矩阵: 为 AOT 的表达式/语句类型建立副作用表格,精确标记哪些操作必须保留
  2. 逃逸感知 DCE: 结合逃逸分析,消除未逃逸对象的操作——这是 AOT 最大的优化机会之一
  3. Control-dependence based DCE: PHP 明确表示当前的 DCE 没有考虑控制依赖(dce.c:35-39 的注释),AOT 可以做更精确的控制依赖 DCE

6. 逃逸分析 (Escape Analysis)

算法 (escape_analysis.c)

基于 Kotzmann & Mossenbock (PPPJ'05) 的经典逃逸分析算法。

核心步骤:

  1. 构建等价逃逸集 (zend_build_equi_escape_sets): 使用 Union-Find 算法。如果两个 SSA 变量通过 φ 函数或 ASSIGN 关联(值相同),它们属于同一个等价类。

  2. 逃逸状态传播: 每个等价类有四种状态:

    ESCAPE_STATE_UNKNOWN      → 初始状态(零初始化的 C 内存)
    ESCAPE_STATE_NO_ESCAPE    → 确定不逃逸(最终目标)
    ESCAPE_STATE_FUNCTION_ESCAPE → 逃逸到被调用函数(传入参数)
    ESCAPE_STATE_GLOBAL_ESCAPE → 全局逃逸(返回、赋值到全局变量、抛异常等)
    
  3. 状态单调收敛: 状态只能从 UNKNOWN → NO_ESCAPE/FUNCTION_ESCAPE/GLOBAL_ESCAPE,从不反转。

  4. 应用逃逸信息:

    • 不逃逸的数组可以在栈上分配(不需要堆分配)
    • 不逃逸的对象可以避免引用计数操作
    • 不逃逸的变量不需要分离(ZEND_SEPARATE)

前驱/后继边: 支持符号类型别名(SYMTABLE_ALIAS)和 HTTP 响应头别名(HTTP_RESPONSE_HEADER_ALIAS)。

AOT 借鉴

逃逸分析对 AOT 编译器可能价值最大:

  1. Box allocation 消除: AOT 使用 Box<T> 表示对象引用。逃逸分析可以确认哪些 Box 不需要堆分配,直接在栈上创建。

  2. 引用计数消除: 不逃逸的对象可以跳过 php::Object::Ref() / php::Object::Unref() 操作。

  3. Array 栈分配: 局部数组逃逸分析后可以改用栈上的 zend_array

  4. 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;       // 推断出的返回类型
};

关键功能:

  1. 双向图: caller_infocallee_info 分别是不同的链表,支持向上(从 callee 找 caller)和向下(从 caller 找 callee)遍历
  2. call_map: 数组索引 opnum → call_info,O(1) 查找某个 opcode 位置对应的调用信息
  3. 返回类型传播: 被调用函数的 return_info 可以向上传播到调用者的 return_info
  4. 参数类型传播: 调用者的实参类型可以向下传播到被调用者的参数类型(用于更精确的函数体优化)

高级跨函数优化 (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)天然构建了完整的符号依赖图。可以在此之上增加:

  1. call_map 索引: 从每个 call site 快速查找 callee 的元信息(参数类型、返回类型、是否内联候选)
  2. 返回类型双向传播: 目前 AOT 的返回类型推导是自顶向下的;调用图允许从 callee 的已知返回类型回传给 caller
  3. 递归标记: 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 借鉴

  1. IR 作为 AST→C++ 的中间载体: AOT 当前直接从 AST 生成 C++ 代码。引入 IR 层可以:

    • 在 IR 层面做优化(fold、GCM、寄存器分配模拟)
    • 解耦前端(PHP AST)和后端(C++ codegen)
  2. ir_fold.h 的常量折叠表: IR 包含一个自动生成的折叠规则表 (gen_ir_fold_hash),定义了数百条代数简化规则。AOT 可以采用类似的"规则表" 驱动的常量折叠。

  3. 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:两阶段序列化——

  1. _calc 计算所需的共享内存大小
  2. _persist 进行实际序列化(所有指针调整为绝对偏移量)

文件缓存 (zend_file_cache.c): 将持久化后的脚本写入文件,允许跨进程重启复用。

AOT 借鉴

当前 AOT 编译器每次从 PHP 源码开始编译。可以借鉴 OPcache 的理念:

  1. 缓存优化后的 AST/类型信息:convert() 阶段之后,将带类型的 AST 序列化,下次编译时直接加载
  2. 增量编译: 只重新编译改动的文件及其依赖
  3. _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 + 类型传播

每一层都可以独立实施并立即给现有代码生成带来收益。