You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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:
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); }
$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.
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.
After each call the code calls phpc_jit_abort_if_pending_type_error.
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.h — fast_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().
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.
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
Category
Regression:· generated-code performance · child of #36188Problem
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 (master4eed6a2785) — both run inphp-compiler:22.04-dev, same box, same minute:fibo(30)wall (best of 3)fibo_rinstructions (objdump)benchmarks/README.mdstill claimsfibo(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); }$n - 2is lowered byJitLongArithOverflow::binaryNativeLong()(lib/JIT/JitLongArithOverflow.php ~L60): it takes two native i64 and returnsnew Variable(..., Variable::TYPE_VALUE, ...)— a boxed__value__stack slot written via__value__writeLong/__value__writeDouble. The hot path never staysTYPE_NATIVE_LONG.+, thereturn) 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 fromJitValueBox::copyBetweenPointers(lib/JIT/JitValueBox.php:638) inlined at each site.phpc_jit_abort_if_pending_type_error.1into a module-globalphpc_scope_var_init_<hash>flag and every read of the parameter$ntests it and has a cold__compiler_trigger_errorpath (undefined-variable guard; separate issue).Introduced by
89867d206f"AOT: promote integer + / * overflow to float (#31964) (#31980)" 2026-08-18 andd061aeff58"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
fast_long_add_function/ZEND_SIGNED_ADD_OVERFLOW: the fast path staysIS_LONGand only the overflow branch materialises a double. Zend's JIT does the same withjoto a cold stub.PHP implementation target
lib/JIT/JitLongArithOverflow.php::binaryNativeLong— returnTYPE_NATIVE_LONGfrom the non-overflow block; move the double materialisation into a cold block that only boxes when overflow actually happens. Where the consumer is itself typedint(typed param /: intreturn understrict_types), the overflow branch must raise the sameTypeErrorZend raises for a float→int coercion failure, so no box is needed at all.llvm.sadd.with.overflow.i64/ssub/smulintrinsics instead of the hand-rolled 8-instruction sign test insignedAddOverflow()/signedSubOverflow().JitValueNumeric.php/Helper.php(touched by AOT: integer overflow promotes to float on + and * (#31964) #31980) soTYPE_NATIVE_LONG ⊙ TYPE_NATIVE_LONGnever yieldsTYPE_VALUEunless overflow occurred at runtime.TYPE_VALUEoperands — this is the "speculation with guards" shape AGENTS.md asks for, applied to the case where the guard is the overflow flag.Repro
Today: ~0.13 s vs Zend ~0.14 s, ~400 instructions. Expected: ≤0.02 s, <60 instructions, no
__value__*calls infibo_r.Done when
fibo_rdisassembly contains no__value__read*/write*and no__string__separatecall;add/subuse the overflow intrinsics with a cold overflow blockfibo(30)AOT ≤ 0.02 s andfibo(32)≥ 5x faster than Zend in the pinned image (regeneratebenchmarks/README.mdtable with output verified)test/compliance/cases/language/int_arith_overflow_promote.phpt,IntArithOverflowPromote31964*Teststay green;PHP_INT_MAX + 1still printsfloat(9.2233720368548E+18)script/aot-smoke.sh8/8;script/differential-sweep.sh --aot --repeat 3failing-name set unchanged vs master