Category
Foundation: · generated-code performance (the "speculation with guards" direction in AGENTS.md) · child of #36188
Problem
AGENTS.md: "untyped loops / calls 4–14x slower than Zend". Today's measurements (master 4eed6a2785, pinned image, outputs verified): benchmarks/simple.php AOT 0.228 s vs Zend 0.092 s (2.5x slower; 3 M calls to strlen/user functions with a string param); mandelbrot 0.436 s vs 0.197 s (2.2x slower; float loop through boxed temporaries). Untyped $a + $b lowers to ~11 basic blocks (JitValueNumeric::emitBoxedNumericResult, lib/JIT/JitValueNumeric.php:373-448) loading each operand's tag four times; untyped params/returns travel as __value__ by value (lib/JIT.php:5003-5070). Existing fast paths are narrow: DiscardedPureCallElision, IncDecResourceProvenance, non-escaping fixed-size arrays → [N x i64] (Analyzer::canEscape + Variable::fromOp:729-742), typed ABI.
Static type inference is a known dead end for PHP (HPHPc); the shape that works (YJIT, V8, Zend's tracing JIT) is: guess from the first observed/likely type, guard once, run a specialised body, bail to the generic body on guard failure.
PHP implementation target
- Loop-counter speculation (L): for a loop whose counter box is assigned from an int literal and only
++/+=/compared against ints in the body, emit if (tag == LONG) once at loop entry, run the body on an i64 phi, write back at exit or on any escape (call passing the var by ref, $$, extract, compact, closure use (&$x)); the else arm is today's boxed body. Reuse Analyzer::canEscape.
- Call-argument speculation (M): for calls to user functions with untyped params where every call site in the module passes the same scalar kind (the compiler sees the whole program), emit a specialised clone
f$i64 guarded at the call site by the arg's tag, keeping f generic. Same for return values.
- Tag caching (S): in
emitBoxedNumericResult load each tag once; ≤ 6 blocks for $a + $b.
Each step ships behind PHP_COMPILER_SPECULATE=loop,call with the generic path untouched, and is measured by the bench gate.
Repro
./script/docker-exec.sh -- bash -lc 'source script/php-env.sh && cat > build/u.php <<'"'"'EOP'"'"'
<?php function f($a, $b) { return $a + $b; } $s = 0; for ($i = 0; $i < 2000000; $i++) { $s = f($s, $i); } echo $s, "\n";
EOP
php bin/compile.php -o build/u build/u.php && time ./build/u && time php build/u.php'
Done when
Category
Foundation:· generated-code performance (the "speculation with guards" direction in AGENTS.md) · child of #36188Problem
AGENTS.md: "untyped loops / calls 4–14x slower than Zend". Today's measurements (master
4eed6a2785, pinned image, outputs verified):benchmarks/simple.phpAOT 0.228 s vs Zend 0.092 s (2.5x slower; 3 M calls tostrlen/user functions with astringparam);mandelbrot0.436 s vs 0.197 s (2.2x slower; float loop through boxed temporaries). Untyped$a + $blowers to ~11 basic blocks (JitValueNumeric::emitBoxedNumericResult, lib/JIT/JitValueNumeric.php:373-448) loading each operand's tag four times; untyped params/returns travel as__value__by value (lib/JIT.php:5003-5070). Existing fast paths are narrow:DiscardedPureCallElision,IncDecResourceProvenance, non-escaping fixed-size arrays →[N x i64](Analyzer::canEscape + Variable::fromOp:729-742), typed ABI.Static type inference is a known dead end for PHP (HPHPc); the shape that works (YJIT, V8, Zend's tracing JIT) is: guess from the first observed/likely type, guard once, run a specialised body, bail to the generic body on guard failure.
PHP implementation target
++/+=/compared against ints in the body, emitif (tag == LONG)once at loop entry, run the body on ani64phi, write back at exit or on any escape (call passing the var by ref,$$,extract,compact, closureuse (&$x)); the else arm is today's boxed body. ReuseAnalyzer::canEscape.f$i64guarded at the call site by the arg's tag, keepingfgeneric. Same for return values.emitBoxedNumericResultload each tag once; ≤ 6 blocks for$a + $b.Each step ships behind
PHP_COMPILER_SPECULATE=loop,callwith the generic path untouched, and is measured by the bench gate.Repro
Done when
benchmarks/simple.phpwithin 1.5x of Zend under AOT;mandelbrot≤ Zend (record all three inbenchmarks/README.md)$i += 0.5) or is captured by reference matches Zend byte-for-byte (test/differential/cases/)script/differential-sweep.sh --aot --repeat 3unchanged; bench gate green