Skip to content

Perf: untyped loop counters and untyped user calls run boxed through __value__ with per-op type dispatch — add guarded speculation (unboxed i64 loop phis, unboxed call arguments) with the generic path as bail-out (lib/JIT.php, lib/JIT/JitValueNumeric.php, lib/JIT/Call/Native.php) (re-#23483) #36217

Description

@PurHur

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

  1. 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.
  2. 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.
  3. 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

  • The untyped probe and benchmarks/simple.php within 1.5x of Zend under AOT; mandelbrot ≤ Zend (record all three in benchmarks/README.md)
  • A bail-out test: the same loop where the counter becomes a float mid-way ($i += 0.5) or is captured by reference matches Zend byte-for-byte (test/differential/cases/)
  • script/differential-sweep.sh --aot --repeat 3 unchanged; bench gate green

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    IMPORTANTCritical patharea:compilerCompiler / CFG / JITenhancementNew feature or requestimplementation-readySpec complete: repro, php-src ref, done-when — safe for workers to claimphase-3:aotPhase 3 – AOT deployment

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions