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.
682 lines
30 KiB
682 lines
30 KiB
# PHP Zend Optimizer / OPcache / JIT Design Analysis: Mechanisms That Can Be Introduced into an AOT Compiler
|
|
|
|
This document analyzes the implementation of the Zend Optimizer, OPcache, and JIT in php-src (v8.4.14), identifying the design patterns, algorithms, and data-flow frameworks that can be introduced into an AOT compiler.
|
|
|
|
Source locations:
|
|
- Optimizer: `~/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/`
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
PHP's optimizer is one of the most mature high-end optimizers for dynamic languages in the field. It contains complete compiler optimization infrastructure, including full SSA (Static Single Assignment) construction, e-SSA (Extended SSA) type/range inference, SCCP (Sparse Conditional Constant Propagation), escape analysis, dead code elimination, call graph analysis, profile-guided tracing JIT, and more.
|
|
|
|
An AOT compiler can borrow from the following aspects:
|
|
|
|
| Priority | Design/Module | Implementation Scale | Benefit |
|
|
|--------|----------|---------|------|
|
|
| P1 | Pass Pipeline architecture | ~100 lines of framework | Clear architecture, pluggable, split into O0/O1/O2 |
|
|
| P1 | SSA + e-SSA construction | Medium | Foundation for all advanced optimizations |
|
|
| P1 | Type inference (Type & Range Inference) | Medium | Precise type deduction capability |
|
|
| P2 | SCDF generic data-flow framework | ~300 lines | Reusable for SCCP/type inference/optimization |
|
|
| P2 | SCCP constant propagation | Medium | Conditional constant folding + unreachable code elimination |
|
|
| P2 | DCE dead code elimination | ~400 lines | Worklist-driven precise DCE |
|
|
| P3 | Escape Analysis | ~500 lines | Stack allocation, reference count elimination |
|
|
| P3 | Call Graph | ~400 lines | Cross-function analysis, inlining decisions, dead code |
|
|
| P4 | JIT IR framework | Very heavy | Reference the abstraction levels of its IR design |
|
|
| P4 | OPcache persistence/File Cache | Light | Serialized IR cache after optimization |
|
|
|
|
---
|
|
|
|
## 1. Pass Pipeline Architecture
|
|
|
|
### Design
|
|
|
|
PHP defines 16 optimization passes, each corresponding to a bitmask bit (`zend_optimizer.h:28-46`):
|
|
|
|
```c
|
|
#define ZEND_OPTIMIZER_PASS_1 (1<<0) // Simple local optimization (constant replacement/folding)
|
|
#define ZEND_OPTIMIZER_PASS_2 (1<<1) //
|
|
#define ZEND_OPTIMIZER_PASS_3 (1<<2) // Jump optimization
|
|
#define ZEND_OPTIMIZER_PASS_4 (1<<3) // INIT_FCALL_BY_NAME -> DO_FCALL
|
|
#define ZEND_OPTIMIZER_PASS_5 (1<<4) // CFG optimization (block pass)
|
|
#define ZEND_OPTIMIZER_PASS_6 (1<<5) // DFA optimization (type/range inference → single function)
|
|
#define ZEND_OPTIMIZER_PASS_7 (1<<6) // CALL GRAPH optimization (cross-function analysis)
|
|
#define ZEND_OPTIMIZER_PASS_8 (1<<7) // SCCP (constant propagation)
|
|
#define ZEND_OPTIMIZER_PASS_9 (1<<8) // Temporary variable optimization
|
|
#define ZEND_OPTIMIZER_PASS_10 (1<<9) // NOP removal
|
|
#define ZEND_OPTIMIZER_PASS_11 (1<<10) // Merge identical constants
|
|
#define ZEND_OPTIMIZER_PASS_12 (1<<11) // Adjust stack usage
|
|
#define ZEND_OPTIMIZER_PASS_13 (1<<12) // Remove unused variables
|
|
#define ZEND_OPTIMIZER_PASS_14 (1<<13) // DCE (dead code elimination)
|
|
#define ZEND_OPTIMIZER_PASS_15 (1<<14) // Collect constants (unsafe)
|
|
#define ZEND_OPTIMIZER_PASS_16 (1<<15) // Function inlining
|
|
```
|
|
|
|
### Pipeline Scheduling
|
|
|
|
The `zend_optimize()` function (`zend_optimizer.c:1067-1183`) executes each pass in order, with each pass only processing the results produced by already-executed passes:
|
|
|
|
```
|
|
pass1 (constant folding) → pass3 (jump optimization) → pass4 (function call optimization)
|
|
→ pass5 (CFG) → pass6 (DFA + type inference) → pass9 (temporary variables)
|
|
→ pass10 (NOP removal) → pass11 (constant merging) → pass13 (variable cleanup) → ...
|
|
```
|
|
|
|
When `PASS_6 + PASS_7` are enabled simultaneously, `zend_optimize_script()` takes a more complex call graph path:
|
|
```
|
|
build_call_graph → zend_optimize (per-func) → analyze_call_graph
|
|
→ build 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
|
|
```
|
|
|
|
### Key Design Points
|
|
|
|
**1. Bitmask switches + registered passes**
|
|
|
|
Users can combine arbitrary passes by bitmask. It also supports `zend_optimizer_register_pass()` to register external passes (such as JIT's own optimization passes):
|
|
|
|
```c
|
|
static struct {
|
|
zend_optimizer_pass_t pass[ZEND_OPTIMIZER_MAX_REGISTERED_PASSES];
|
|
int last;
|
|
} zend_optimizer_registered_passes;
|
|
```
|
|
|
|
Registered passes execute after all built-in passes (`zend_optimizer_call_registered_passes`).
|
|
|
|
**2. Optional dump output per pass**
|
|
|
|
Controlled via `debug_level`, supporting output of the intermediate representation before/after any pass for debugging and performance analysis.
|
|
|
|
**3. Dual-layer optimization: per-function and script-level**
|
|
|
|
- `zend_optimize(op_array, ctx)` — single-function optimization, conservative (does not use cross-function information)
|
|
- `zend_optimize_script(script, ...)` — whole-script optimization with call graph, enabling cross-function optimization
|
|
|
|
### AOT Takeaways
|
|
|
|
The AOT compiler can define a similar pass pipeline:
|
|
|
|
```php
|
|
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;
|
|
}
|
|
```
|
|
|
|
Combined by optimization level:
|
|
|
|
```php
|
|
const O0 = AotPass::TYPE_CHECK_INSERT->value; // Required baseline code generation
|
|
const O1 = O0 | AotPass::CONSTANT_FOLD->value; // Basic optimization
|
|
const O2 = O1 | AotPass::DEVIRTUALIZE->value | AotPass::FUNCTION_INLINE->value;
|
|
```
|
|
|
|
---
|
|
|
|
## 2. SSA (Static Single Assignment) + e-SSA
|
|
|
|
### Data Structures
|
|
|
|
SSA is built on top of the control flow graph (CFG):
|
|
|
|
**CFG (`zend_cfg.h:84-92`):**
|
|
```c
|
|
typedef struct _zend_cfg {
|
|
int blocks_count; // Number of basic blocks
|
|
int edges_count; // Number of edges
|
|
zend_basic_block *blocks; // Array of basic blocks
|
|
int *predecessors; // Predecessor list
|
|
uint32_t *map; // opnum → block mapping
|
|
uint32_t flags;
|
|
} zend_cfg;
|
|
|
|
typedef struct _zend_basic_block {
|
|
int *successors; // Successor block indices
|
|
uint32_t flags;
|
|
uint32_t start; // Starting opcode
|
|
uint32_t len; // Number of opcodes
|
|
int successors_count;
|
|
int predecessors_count;
|
|
int idom; // Immediate dominator
|
|
int loop_header; // Nearest loop header
|
|
int level; // Dominator tree depth
|
|
int children; // Linked list of dominated child blocks
|
|
} zend_basic_block;
|
|
```
|
|
|
|
**SSA (`zend_ssa.h:135-143`):**
|
|
```c
|
|
typedef struct _zend_ssa {
|
|
zend_cfg cfg; // Control flow graph
|
|
int vars_count; // Number of SSA variables
|
|
int sccs; // Number of strongly connected components
|
|
zend_ssa_block *blocks; // φ functions per basic block
|
|
zend_ssa_op *ops; // use-def information per instruction
|
|
zend_ssa_var *vars; // def-use chain per SSA variable
|
|
zend_ssa_var_info *var_info; // Type inference results (type bitmask + range)
|
|
} zend_ssa;
|
|
```
|
|
|
|
**SSA Op (`zend_ssa.h:82-92`):**
|
|
```c
|
|
typedef struct _zend_ssa_op {
|
|
int op1_use;
|
|
int op2_use;
|
|
int result_use;
|
|
int op1_def; // SSA variable defined by this instruction
|
|
int op2_def;
|
|
int result_def;
|
|
int op1_use_chain; // use-def chain
|
|
int op2_use_chain;
|
|
int res_use_chain;
|
|
} zend_ssa_op;
|
|
```
|
|
|
|
### e-SSA: Extended SSA with Pi Nodes
|
|
|
|
This is one of the most elegant designs of the PHP optimizer. A Pi node is a special kind of φ function used to represent type/range constraints inferred from conditional branches.
|
|
|
|
**Pi constraints (`zend_ssa.h:42-59`):**
|
|
```c
|
|
typedef struct _zend_ssa_range_constraint {
|
|
zend_ssa_range range; // Range constraint [min, max]
|
|
int min_var; // Symbolic lower-bound variable
|
|
int max_var; // Symbolic upper-bound variable
|
|
zend_ssa_negative_lat negative; // Negation potential
|
|
} zend_ssa_range_constraint;
|
|
|
|
typedef struct _zend_ssa_type_constraint {
|
|
uint32_t type_mask; // Type mask (narrowed type after AND operation)
|
|
zend_class_entry *ce; // Class entry (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;
|
|
```
|
|
|
|
**How it works:** For a condition like `if ($x > 0)`:
|
|
- In the truthy branch, insert `Pi($x, range[1, LONG_MAX])` — constrain `$x`'s SSA variable to the range > 0
|
|
- In the falsy branch, insert `Pi($x, range[LONG_MIN, 0])` — constrain `$x` to ≤ 0
|
|
|
|
This allows subsequent optimizations within a branch to use refined type/range information without altering the explicit assignment chain of the original variable.
|
|
|
|
### SSA Construction Flow
|
|
|
|
```
|
|
1. zend_build_cfg() → Build the control flow graph (including dominator tree, loop detection)
|
|
2. zend_build_dfg() → Build the data flow graph (compute use/def sets)
|
|
3. zend_build_ssa() → Place φ functions → rename variables → build SSA form
|
|
4. zend_ssa_compute_use_def_chains() → Connect use-def chains
|
|
5. zend_ssa_find_sccs() → Find strongly connected components (for type inference)
|
|
6. zend_ssa_inference() → Type inference + range inference (populate var_info)
|
|
```
|
|
|
|
### AOT Takeaways
|
|
|
|
The AOT compiler does not need SSA form (because it generates C++ code, not direct register manipulation), but the following e-SSA concepts can be used directly:
|
|
|
|
1. **The Pi constraint concept:** Insert type-narrowing markers in conditional branches so that variables inside a branch have more precise types. This directly corresponds to the implementation basis of the TypeSpecifier / Type Narrowing (#7) mentioned earlier.
|
|
|
|
2. **Type & Range information associated with every expression:** Similar to the SSA `var_info` design, AOT can maintain a `{type_mask, range, ce}` triple for every variable/expression in the FunctionContext.
|
|
|
|
3. **Use-def chains for optimization decisions:** When determining whether a variable has exactly one `use`, SSA's use_chain provides O(1) lookup.
|
|
|
|
---
|
|
|
|
## 3. Type Inference (Type & Range Inference)
|
|
|
|
### The Type System
|
|
|
|
PHP uses a bitmask to represent type information (defined in `zend_type_info.h`), which is its most distinctive design:
|
|
|
|
```c
|
|
#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)
|
|
|
|
// Convenient combinations
|
|
#define MAY_BE_ANY (MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|...)
|
|
#define MAY_BE_TRUTHY (MAY_BE_TRUE|MAY_BE_LONG|... /* not 0/''/[]/null */)
|
|
#define MAY_BE_FALSEY (MAY_BE_UNDEF|MAY_BE_NULL|MAY_BE_FALSE|...)
|
|
```
|
|
|
|
**Core advantage:** Bit operations are extremely fast. Type operations (merge/intersection/difference) require only a single AND/OR/NOT instruction:
|
|
|
|
```c
|
|
// Merge the types of two variables
|
|
uint32_t result_type = info1 | info2;
|
|
|
|
// Check whether it might be a string
|
|
if (info & MAY_BE_STRING) { ... }
|
|
|
|
// Intersection
|
|
uint32_t common = info1 & info2;
|
|
```
|
|
|
|
### Range Inference
|
|
|
|
Each SSA variable carries a `zend_ssa_range { min, max, underflow, overflow }`:
|
|
|
|
```c
|
|
typedef struct _zend_ssa_range {
|
|
zend_long min;
|
|
zend_long max;
|
|
bool underflow; // Whether there is an underflow risk
|
|
bool overflow; // Whether there is an overflow risk
|
|
} zend_ssa_range;
|
|
```
|
|
|
|
The core algorithm (`zend_inference.c:1071`) is based on V. Campos's "Speed and Precision in Range Analysis, SBLP'12" paper:
|
|
|
|
1. **Warmup phase (16 passes):** Propagate ranges over SCCs (strongly connected components), using widening to accelerate convergence
|
|
2. **Narrowing phase:** Gradually narrow the ranges, eliminating the over-approximation caused by widening
|
|
3. **Zend Engine-specific arithmetic semantics:** `zend_add_will_overflow()`, `zend_sub_will_overflow()` and others precisely detect integer overflow
|
|
|
|
**Operator range inference example:**
|
|
|
|
```c
|
|
// ADD: result range
|
|
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())
|
|
|
|
// Result type: if overflow is true, add MAY_BE_DOUBLE to the type
|
|
// (PHP int overflow automatically converts to float)
|
|
```
|
|
|
|
### `update_type_info` per Opcode
|
|
|
|
`_zend_update_type_info()` is a huge switch that precisely computes the result type and range for each Zend opcode. For example, `ZEND_ASSIGN_DIM` (array assignment) updates not only the type of the assigned element, but also the type of the array as a whole, considering MAY_BE_PACKED_GUARD (packed array guard) and reference count inference.
|
|
|
|
### AOT Takeaways
|
|
|
|
1. **Bitmask type system:** It is the most suitable lightweight type representation for the AOT compiler. AOT currently uses string types (`TYPE_INT = 'int'`), which cannot efficiently represent compound types like "may be int or string". Bitmasks provide O(1) union/intersect/test operations.
|
|
|
|
2. **Range inference:** Can select the optimal integer type for C++ code generation (`int32_t` vs `int64_t` vs `BigInt`), avoiding unnecessary BigInt allocations.
|
|
|
|
3. **Overflow tracking:** Precisely determine when conversion from int64 to float/BigInt is needed, inserting conversion code only when overflow is actually possible.
|
|
|
|
4. **Per-opcode type update table:** The design of `_zend_update_type_info()` maps directly onto AOT's Rule system — each opcode corresponds to a Rule responsible for outputting the result type of that operation.
|
|
|
|
---
|
|
|
|
## 4. SCCP (Sparse Conditional Constant Propagation)
|
|
|
|
### Core Design
|
|
|
|
SCCP performs both **constant propagation** and **conditional constant folding** simultaneously, and can also eliminate unreachable code (no separate dead code elimination pass needed).
|
|
|
|
Implemented in `sccp.c`, based on `scdf.h` (the SCDF framework).
|
|
|
|
### Value Lattice
|
|
|
|
```
|
|
TOP (undefined)
|
|
/ | \
|
|
C1 C2 C3 (constant values)
|
|
\ | /
|
|
BOT (overdefined = not constant)
|
|
```
|
|
|
|
- TOP: the value of this variable is not yet known (optimistic assumption)
|
|
- BOT: this variable is known not to be a constant
|
|
- Constant value: the exact value is known
|
|
|
|
### Key Algorithm Points (from the comments at sccp.c:30-74)
|
|
|
|
**The `meet` operation (merging of φ functions):**
|
|
- BOT + any = BOT
|
|
- TOP + any = any
|
|
- C_i + C_i = C_i (two identical constants)
|
|
- C_i + C_j = BOT (two different constants)
|
|
|
|
**Instruction evaluation:**
|
|
- Any operand is BOT → result is BOT (exception: op1 of ASSIGN)
|
|
- Instructions that can never be evaluated → BOT
|
|
- Any operand is TOP → result is TOP
|
|
- All operands are known constants → attempt compile-time evaluation → return constant value on success, BOT on failure
|
|
|
|
**Branch feasibility determination:**
|
|
- Branch on BOT → all successors are feasible
|
|
- Branch on TOP → no successor is infeasible (wait for more information)
|
|
- Branch on a known constant → only the branch that satisfies the condition is feasible
|
|
|
|
### The SCDF Framework (`scdf.h`)
|
|
|
|
SCCP is built on top of the SCDF (Sparse Conditional Data Flow) framework, a generic sparse conditional data-flow analysis engine:
|
|
|
|
```c
|
|
typedef struct _scdf_ctx {
|
|
zend_op_array *op_array;
|
|
zend_ssa *ssa;
|
|
zend_bitset instr_worklist; // Instructions to process
|
|
zend_bitset phi_var_worklist; // Phi/SSA variables to process
|
|
zend_bitset block_worklist; // Blocks to process
|
|
zend_bitset executable_blocks; // Executable blocks
|
|
zend_bitset feasible_edges; // Feasible edges
|
|
|
|
struct {
|
|
void (*visit_instr)(...); // Process an instruction
|
|
void (*visit_phi)(...); // Process a φ function
|
|
void (*mark_feasible_successors)(...); // Mark feasible successors
|
|
} handlers;
|
|
} scdf_ctx;
|
|
```
|
|
|
|
**Usage pattern:** SCCP implements `visit_instr` (constant evaluation), `visit_phi` (the meet operation), and `mark_feasible_successors` (branch feasibility). Type inference also uses a similar worklist propagation algorithm.
|
|
|
|
**Generic worklist mechanism:**
|
|
```c
|
|
// When a variable's value changes, add all its uses to the 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); // Mark instructions using this variable
|
|
}
|
|
FOREACH_PHI_USE(var, phi) {
|
|
zend_bitset_incl(scdf->phi_var_worklist, phi->ssa_var);
|
|
}
|
|
}
|
|
```
|
|
|
|
### AOT Takeaways
|
|
|
|
1. **The SCDF framework is the most directly reusable:** about 300 lines of C code providing a generic worklist-driven conditional data-flow engine. AOT can port it as a PHP class, reusing it across multiple optimization passes such as SCCP, type inference, and escape analysis.
|
|
|
|
2. **The TOP/BOT lattice model:** The AOT compiler can use the same lattice structure when analyzing types:
|
|
- TOP = unknown type (early in analysis)
|
|
- BOT = contradictory type (an inconsistency was found)
|
|
- Concrete value (a constant or an exact type)
|
|
|
|
3. **Conditional branch feasibility:** SCCP's branch feasibility determination can directly help AOT eliminate unreachable branches at compile time, generating simpler C++ code.
|
|
|
|
---
|
|
|
|
## 5. DCE (Dead Code Elimination)
|
|
|
|
### Algorithm (`dce.c`)
|
|
|
|
PHP's DCE uses an optimistic strategy:
|
|
|
|
```
|
|
1. Assume all instructions and φ functions are dead
|
|
2. Mark all instructions with obvious side effects as live (side-effect instructions)
|
|
3. Starting from live instructions, mark the defining instructions of their operands as live (reverse propagation along use-def chains)
|
|
4. Repeat until the worklist is empty
|
|
5. Delete all instructions still marked as dead
|
|
```
|
|
|
|
**The key `may_have_side_effects()` check (`dce.c:74-100`):**
|
|
|
|
Zend opcodes are divided into three kinds:
|
|
- Never have side effects (such as ADD, CONCAT, BOOL_NOT): can be eliminated by DCE
|
|
- May produce a notice but have no essential side effect (such as DIV_BY_ZERO triggering a warning): configurable whether to eliminate
|
|
- Always have side effects (such as ECHO, THROW, ASSIGN_OBJ): must be preserved
|
|
|
|
**Special capability:** It can eliminate "redundant modifications to non-escaping arrays/objects" and "useless array/object allocations". If an array is only built, modified, and used locally, the intermediate ASSIGN_DIM steps may be eliminated.
|
|
|
|
### AOT Takeaways
|
|
|
|
The AOT compiler's DCE can be more aggressive (because types are known at compile time):
|
|
|
|
1. **Side-effect classification matrix:** Build a side-effect table for AOT's expression/statement types, precisely marking which operations must be preserved
|
|
2. **Escape-aware DCE:** Combined with escape analysis, eliminate operations on non-escaping objects — this is one of AOT's biggest optimization opportunities
|
|
3. **Control-dependence based DCE:** PHP explicitly states that its current DCE does not consider control dependence (comments at `dce.c:35-39`); AOT can perform more precise control-dependence DCE
|
|
|
|
---
|
|
|
|
## 6. Escape Analysis
|
|
|
|
### Algorithm (`escape_analysis.c`)
|
|
|
|
Based on the classic escape analysis algorithm of Kotzmann & Mossenbock (PPPJ'05).
|
|
|
|
**Core steps:**
|
|
|
|
1. **Build equivalence escape sets (`zend_build_equi_escape_sets`):** Uses the Union-Find algorithm. If two SSA variables are related through a φ function or ASSIGN (same value), they belong to the same equivalence class.
|
|
|
|
2. **Escape state propagation:** Each equivalence class has four states:
|
|
```
|
|
ESCAPE_STATE_UNKNOWN → initial state (zero-initialized C memory)
|
|
ESCAPE_STATE_NO_ESCAPE → definitely does not escape (final goal)
|
|
ESCAPE_STATE_FUNCTION_ESCAPE → escapes to the called function (passed as argument)
|
|
ESCAPE_STATE_GLOBAL_ESCAPE → global escape (returned, assigned to a global variable, throws an exception, etc.)
|
|
```
|
|
|
|
3. **Monotonic state convergence:** States can only go from UNKNOWN → NO_ESCAPE/FUNCTION_ESCAPE/GLOBAL_ESCAPE, never reverse.
|
|
|
|
4. **Apply escape information:**
|
|
- Non-escaping arrays can be allocated on the stack (no heap allocation needed)
|
|
- Non-escaping objects can avoid reference counting operations
|
|
- Non-escaping variables do not need separation (ZEND_SEPARATE)
|
|
|
|
**Predecessor/successor edges:** Supports symbolic type aliases (SYMTABLE_ALIAS) and HTTP response header aliases (HTTP_RESPONSE_HEADER_ALIAS).
|
|
|
|
### AOT Takeaways
|
|
|
|
Escape analysis may be the most valuable for the AOT compiler:
|
|
|
|
1. **Box allocation elimination:** AOT uses `Box<T>` to represent object references. Escape analysis can confirm which Boxes do not need heap allocation and can be created directly on the stack.
|
|
|
|
2. **Reference count elimination:** Non-escaping objects can skip `php::Object::Ref()` / `php::Object::Unref()` operations.
|
|
|
|
3. **Array stack allocation:** After escape analysis, local arrays can use a stack-based `zend_array`.
|
|
|
|
4. **The 4-state model is very simple and effective**, and AOT can map it directly:
|
|
- ESCAPE_STATE_NO_ESCAPE → stack allocation
|
|
- ESCAPE_STATE_FUNCTION_ESCAPE → decided by the caller
|
|
- ESCAPE_STATE_GLOBAL_ESCAPE → heap allocation
|
|
|
|
---
|
|
|
|
## 7. Call Graph
|
|
|
|
### Design (`zend_call_graph.h`)
|
|
|
|
PHP's call graph tracks bidirectional relationships, both caller → callee and callee → caller:
|
|
|
|
```c
|
|
struct _zend_call_info {
|
|
zend_op_array *caller_op_array; // Caller
|
|
zend_op *caller_init_opline; // INIT_FCALL instruction
|
|
zend_op *caller_call_opline; // DO_FCALL instruction
|
|
zend_function *callee_func; // Called function
|
|
zend_call_info *next_caller; // Linked list: the callee's next caller
|
|
zend_call_info *next_callee; // Linked list: the caller's next callee
|
|
bool recursive; // Recursive call
|
|
bool send_unpack; // Uses SEND_UNPACK
|
|
bool named_args; // Named arguments
|
|
bool is_prototype; // May be a method overridden by a subclass
|
|
bool is_frameless; // frameless function
|
|
int num_args;
|
|
zend_send_arg_info arg_info[1];
|
|
};
|
|
|
|
struct _zend_func_info {
|
|
zend_ssa ssa; // The function's own SSA
|
|
zend_call_info *caller_info; // Who called this function
|
|
zend_call_info *callee_info; // Whom this function called
|
|
zend_call_info **call_map; // Quick index from opnum to call_info
|
|
zend_ssa_var_info return_info; // Inferred return type
|
|
};
|
|
```
|
|
|
|
**Key features:**
|
|
|
|
1. **Bidirectional graph:** `caller_info` and `callee_info` are separate linked lists, supporting traversal upward (from callee to find callers) and downward (from caller to find callees)
|
|
2. **call_map:** An array indexing opnum → call_info, providing O(1) lookup of the call information for a given opcode position
|
|
3. **Return type propagation:** The callee's return_info can be propagated upward to the caller's return_info
|
|
4. **Argument type propagation:** The caller's actual argument types can be propagated downward to the callee's parameter types (for more precise function-body optimization)
|
|
|
|
### Advanced Cross-Function Optimization (`zend_optimize_script:1626-1728`)
|
|
|
|
```
|
|
1. build_call_graph → build the bidirectional call graph
|
|
2. zend_optimize (per-func) → perform independent local optimization per function
|
|
3. analyze_call_graph → infer function information (recursion flags, indirect variable access, func_get_args, etc.)
|
|
4. build_call_map → build the opnum→call index for each function
|
|
5. dfa_analyze_op_array → build SSA + type inference (per-func)
|
|
6. dfa_optimize_op_array → perform SCCP + DCE + block pass based on SSA
|
|
```
|
|
|
|
### AOT Takeaways
|
|
|
|
The AOT compiler's first two steps (prepare + convert) naturally build a complete symbol dependency graph. On top of this, it can add:
|
|
|
|
1. **call_map index:** Quickly look up the callee's metadata from each call site (parameter types, return type, whether it is an inlining candidate)
|
|
2. **Bidirectional return type propagation:** AOT's return type inference is currently top-down; the call graph allows feeding the callee's known return type back to the caller
|
|
3. **Recursion flags:** `ZEND_FUNC_RECURSIVE_DIRECTLY` / `ZEND_FUNC_RECURSIVE_INDIRECTLY` are critical for inlining strategy decisions
|
|
|
|
---
|
|
|
|
## 8. JIT IR Framework
|
|
|
|
### Design
|
|
|
|
The IR (Intermediate Representation) used by PHP JIT is a generic SSA-derived low-level intermediate representation, located in `ext/opcache/jit/ir/`.
|
|
|
|
**Three stages of IR:**
|
|
|
|
| Stage | Files | Purpose |
|
|
|------|------|------|
|
|
| IR builder | `ir_builder.h`, `zend_jit_ir.c` | Build IR instructions from Zend bytecode |
|
|
| IR optimizer | `ir_cfg.c`, `ir_fold.h`, `ir_gcm.c` | CFG optimization, constant folding, global code motion (GCM) |
|
|
| IR emitter | `ir_emit.c`, `ir_emit_x86.h` | Emit x86/ARM64 machine code from IR |
|
|
|
|
**Example IR instructions: IR_ADD, IR_MUL, IR_LOAD, IR_STORE, IR_CALL, IR_GUARD, etc.**
|
|
|
|
**JIT optimization levels (`zend_jit.h:32-37`):**
|
|
```c
|
|
#define ZEND_JIT_LEVEL_NONE 0 // JIT not enabled
|
|
#define ZEND_JIT_LEVEL_MINIMAL 1 // Minimal JIT (subroutine threading)
|
|
#define ZEND_JIT_LEVEL_INLINE 2 // Selective inline threading
|
|
#define ZEND_JIT_LEVEL_OPT_FUNC 3 // Optimize a single function based on type inference
|
|
#define ZEND_JIT_LEVEL_OPT_FUNCS 4 // Optimize based on the call tree
|
|
#define ZEND_JIT_LEVEL_OPT_SCRIPT 5 // Interprocedural analysis
|
|
```
|
|
|
|
**JIT trigger modes (`zend_jit.h:39-44`):**
|
|
```c
|
|
#define ZEND_JIT_ON_SCRIPT_LOAD 0 // Compile immediately when all functions are loaded
|
|
#define ZEND_JIT_ON_FIRST_EXEC 1 // Compile on first execution
|
|
#define ZEND_JIT_ON_PROF_REQUEST 2 // Compile the hottest functions based on profile data
|
|
#define ZEND_JIT_ON_HOT_COUNTERS 3 // Compile after N calls/loop iterations
|
|
#define ZEND_JIT_ON_HOT_TRACE 5 // Use tracing JIT after N calls
|
|
```
|
|
|
|
### AOT Takeaways
|
|
|
|
1. **IR as an intermediate carrier for AST→C++:** AOT currently generates C++ code directly from the AST. Introducing an IR layer can:
|
|
- Perform optimization at the IR level (fold, GCM, register allocation simulation)
|
|
- Decouple the front end (PHP AST) from the back end (C++ codegen)
|
|
|
|
2. **The constant folding table of `ir_fold.h`:** IR contains an auto-generated folding rule table (`gen_ir_fold_hash`) defining hundreds of algebraic simplification rules. AOT can adopt a similar "rule table"-driven constant folding approach.
|
|
|
|
3. **JIT's profiling mechanism:** `hot_loop` / `hot_func` counters — AOT can embed profile data into the generated binary for PGO (Profile-Guided Optimization).
|
|
|
|
---
|
|
|
|
## 9. OPcache Persistence & File Cache
|
|
|
|
### Design
|
|
|
|
OPcache is not just a cache — it stores the **optimized** bytecode in the cache.
|
|
|
|
```
|
|
Original PHP source code
|
|
→ compiled to zend_op_array (original bytecode)
|
|
→ through all Zend Optimizer passes (SSA + type inference + SCCP + DCE + ...)
|
|
→ keep only the optimized zend_op_array (discard temporary IR such as SSA)
|
|
→ zend_persist() serializes to shared memory / file cache
|
|
```
|
|
|
|
**`zend_persist_calc` + `zend_persist`**: two-phase serialization —
|
|
1. `_calc` computes the required shared memory size
|
|
2. `_persist` performs the actual serialization (all pointers adjusted to absolute offsets)
|
|
|
|
**File cache (`zend_file_cache.c`):** Writes the persisted script to a file, allowing reuse across process restarts.
|
|
|
|
### AOT Takeaways
|
|
|
|
The current AOT compiler compiles from PHP source code every time. It can borrow the OPcache philosophy:
|
|
|
|
1. **Cache the optimized AST/type information:** After the `convert()` stage, serialize the typed AST and load it directly on the next compilation
|
|
2. **Incremental compilation:** Recompile only changed files and their dependencies
|
|
3. **Two-phase serialization (`_calc` + `_persist`):** Compute the size first, then allocate memory/write, avoiding realloc fragmentation
|
|
|
|
---
|
|
|
|
## 10. Other Notable Designs
|
|
|
|
### zend_bitset
|
|
|
|
PHP uses its own bitset implementation for efficient set operations. The optimizer frequently uses bitsets to represent worklists, live sets, and def/use sets.
|
|
|
|
### zend_worklist.h
|
|
|
|
Generic worklist iteration macros; SCCP and type inference use the same worklist mechanism. Designed as macros for inlining performance.
|
|
|
|
### zend_arena
|
|
|
|
An arena memory allocator used for fast allocation and bulk release of all optimizer data structures. One arena is bound to one `zend_optimizer_ctx`, and all passes share the same arena.
|
|
|
|
### Inter-Pass Data Management
|
|
|
|
Temporary IR such as SSA is destroyed immediately after a pass completes (via arena free), keeping only the optimized results in the final `zend_op_array`. This guarantees memory efficiency.
|
|
|
|
---
|
|
|
|
## Recommended Adoption Order (for the AOT compiler)
|
|
|
|
```
|
|
Phase 1: Pass Pipeline
|
|
└── Define the AotPass enum + Pipeline runner, pluggable pass architecture
|
|
|
|
Phase 2: Bitmask type system
|
|
└── Borrow Zend's type mask design, replacing the current string type constants
|
|
└── Map directly to C++ uint32_t constants
|
|
|
|
Phase 3: Type inference rules
|
|
└── Each AST node/opcode corresponds to an update_type_info
|
|
└── Implement in combination with the Rule system (#4 design)
|
|
|
|
Phase 4: SCCP + DCE
|
|
└── Constant propagation + dead code elimination based on the SCDF framework
|
|
└── Can eliminate redundant expressions before C++ code generation
|
|
|
|
Phase 5: Escape analysis
|
|
└── Union-Find equivalence escape sets + 4-state propagation
|
|
└── Used for Box allocation elimination and reference count elimination
|
|
|
|
Phase 6: Call graph cross-function optimization
|
|
└── Add call_map + type propagation on top of the existing symbol dependency graph
|
|
```
|
|
|
|
Each layer can be implemented independently and immediately bring benefits to existing code generation.
|
|
|