Skip to content

Perf: typed int arithmetic boxes every result since overflow promotion — fibo(30) 8x slower than the 2026-07-28 build (lib/JIT/JitLongArithOverflow.php) #36189

Description

@PurHur

Category

Regression: · generated-code performance · child of #36188

Problem

The flagship "typed recursion is 7–9x faster than Zend" result is gone. The same program shape compiled on 2026-07-28 (build/micro/ackloc/r1_ternary.bin, still on the dev box) and today (master 4eed6a2785) — both run in php-compiler:22.04-dev, same box, same minute:

binary fibo(30) wall (best of 3) fibo_r instructions (objdump) binary size
built 2026-07-28 0.015 s 25 10.9 MB
built 2026-09-01 (master 4eed6a2) 0.13 s ~400 19.5 MB
Zend 8.2 (same box) 0.13–0.15 s

benchmarks/README.md still claims fibo(30) native run 0.0170 s vs Zend 0.1008 s. Nothing in any gate noticed an 8x regression of the headline number for five weeks (see the benchmark-gate sibling issue).

Mechanism (read from the disassembly of today's fibo_r)

function fibo_r(int $n): int { return ($n < 2) ? 1 : fibo_r($n - 2) + fibo_r($n - 1); }

  1. $n - 2 is lowered by JitLongArithOverflow::binaryNativeLong() (lib/JIT/JitLongArithOverflow.php ~L60): it takes two native i64 and returns new Variable(..., Variable::TYPE_VALUE, ...) — a boxed __value__ stack slot written via __value__writeLong / __value__writeDouble. The hot path never stays TYPE_NATIVE_LONG.
  2. Every consumer of that boxed slot (the call argument, the +, the return) now re-dispatches on the type byte: and $0x7f; cmp $4 (string → __value__readString + __string__separate + __value__writeString), cmp $7 (hashtable), cmp $5 (object), cmp $1/$2/$3 … then __value__readLong. That is the 7-way copy switch from JitValueBox::copyBetweenPointers (lib/JIT/JitValueBox.php:638) inlined at each site.
  3. After each call the code calls phpc_jit_abort_if_pending_type_error.
  4. Additionally each function entry stores 1 into a module-global phpc_scope_var_init_<hash> flag and every read of the parameter $n tests it and has a cold __compiler_trigger_error path (undefined-variable guard; separate issue).

Introduced by 89867d206f "AOT: promote integer + / * overflow to float (#31964) (#31980)" 2026-08-18 and d061aeff58 "integer subtraction overflow promotes to float (#32422)" 2026-08-19. The semantics fix is right; the representation chosen for it is what costs 8x.

php-src reference

  • Zend/zend_operators.hfast_long_add_function / ZEND_SIGNED_ADD_OVERFLOW: the fast path stays IS_LONG and only the overflow branch materialises a double. Zend's JIT does the same with jo to a cold stub.

PHP implementation target

  • lib/JIT/JitLongArithOverflow.php::binaryNativeLong — return TYPE_NATIVE_LONG from the non-overflow block; move the double materialisation into a cold block that only boxes when overflow actually happens. Where the consumer is itself typed int (typed param / : int return under strict_types), the overflow branch must raise the same TypeError Zend raises for a float→int coercion failure, so no box is needed at all.
  • Use llvm.sadd.with.overflow.i64 / ssub / smul intrinsics instead of the hand-rolled 8-instruction sign test in signedAddOverflow() / signedSubOverflow().
  • Audit JitValueNumeric.php / Helper.php (touched by AOT: integer overflow promotes to float on + and * (#31964) #31980) so TYPE_NATIVE_LONG ⊙ TYPE_NATIVE_LONG never yields TYPE_VALUE unless overflow occurred at runtime.
  • Keep the generic boxed path as the fallback for TYPE_VALUE operands — this is the "speculation with guards" shape AGENTS.md asks for, applied to the case where the guard is the overflow flag.

Repro

./script/docker-exec.sh -- bash -lc 'source script/php-env.sh \
  && php bin/compile.php -o build/fibo30 "benchmarks/fibo(30).php" \
  && time ./build/fibo30 && time php "benchmarks/fibo(30).php" \
  && objdump -d --no-show-raw-insn --disassemble=fibo_r build/fibo30 | grep -c "^ "'

Today: ~0.13 s vs Zend ~0.14 s, ~400 instructions. Expected: ≤0.02 s, <60 instructions, no __value__* calls in fibo_r.

Done when

  • fibo_r disassembly contains no __value__read*/write* and no __string__separate call; add/sub use the overflow intrinsics with a cold overflow block
  • fibo(30) AOT ≤ 0.02 s and fibo(32) ≥ 5x faster than Zend in the pinned image (regenerate benchmarks/README.md table with output verified)
  • test/compliance/cases/language/int_arith_overflow_promote.phpt, IntArithOverflowPromote31964*Test stay green; PHP_INT_MAX + 1 still prints float(9.2233720368548E+18)
  • script/aot-smoke.sh 8/8; script/differential-sweep.sh --aot --repeat 3 failing-name set unchanged vs master
  • Bench regression gate (sibling issue) green on this shape

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

    MOST IMPORTANTThis are the most important targetsarea:compilerCompiler / CFG / JITbugSomething isn't workingimplementation-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