Repository navigation
Optimize AOT by default (3.5x→1.3x vs Zend), fix exposed miscompiles, php-src exact pow/numeric strings, -146k lines - #37724
Merged
Conversation
) Generated code had never been optimized in a default `phpc build`: OPT_LEVEL>=1 died with a PHP fatal in vendored php-llvm (PassManagerBuilder::populate*PassManager narrowed the interface's parameter type), and the default path skipped IR passes and used OptNone codegen. All `alwaysinline` value accessors stayed out-of-line calls. - patches/php-llvm-pass-manager-builder-core-type.patch: fix the signature. - Internalize every script-module function that peer objects (helper-runtime units, vendor prelink) do not reference (their `nm -u`), then GlobalDCE. ~80% of a small script's IR is unreachable runtime library; only plain standalone links (not chunk/partial/object-only/bitcode emits). - Internalized standalone builds now default to the O2 module pipeline and Default codegen; PHP_COMPILER_OPT_LEVEL=0/none still opts out. - Inliner hygiene: drop `alwaysinline` from helpers > 80 instructions, `minsize` on __init__/__shutdown__/__superglobals__*, `optsize` on functions > 4000 instructions; split __ref__delref into an inline fast path (decrement, still live, not a GC-root candidate) and a cold out-of-line slow path (Zend zval_ptr_dtor shape). - Error-path bloat: the object-operand TypeError for arithmetic and the string-builtin object-arg TypeError emitted one raise arm per known class at every site (~80% of a typical function's IR). Now one raise with the class name from a module-level class-id table (ClassNameTable), message built at runtime; ExceptionBridge/TypeErrorRaise/TryCatchHelper gained runtime-message variants. - CompileCache key now covers every lib/ and ext/ source (path/size/mtime) and the opt-level env knobs; it used to hash a hand-picked handful of files, so most codegen edits restored stale binaries. The cache stamp (module.bc) is saved before internalize/optimize. - aot-smoke IR size gate measured a stale /tmp/phpc-last.ll whenever the echo build was a cache hit; delete it first and bypass the cache. Measured (scaled-up benchmarks, Zend 8.2 in the pinned image, best of 3, output verified identical; these are lab numbers, not a committed table): geomean AOT/Zend over 10 programs 3.5x slower -> 1.3x slower; untyped loop 2.22s -> 0.54s (Zend 0.48s); nbody 1.29s -> 0.41s (Zend 0.41s); spectral-norm 0.65x of Zend; call-heavy 0.23x. Compile time at O2: nbody 404s (unfixed O2) -> ~14s; MiniWebApp 210s -> 38s. Gates run: make check (dev-verify-fast) OK; targeted Zend-vs-AOT checks of the object-operand and string-arg TypeError paths identical to master. AOT differential sweep: running at commit time (see follow-up). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to the optimize-by-default change; the parallel AOT differential sweep found 5 runtime regressions, all fixed here: - Internalize keep-set now includes every global symbol peer objects *define*, not only what they leave undefined. With -z muldefs the script's definition wins for all references, including a helper unit's calls to its own copy; internalizing it rebound those calls to the (stale) unit copy -> crashes in InfoJitHelper/__string__separate (discarded_* cases). - Peer-only functions (kept external for helpers, unreachable from main) get optnone+noinline, so the O2 pipeline is spent on the user program; compile time stays near the unsound-internalize numbers. - Zero-initialize pointer-typed entry allocas. Some lowerings delref a pointer local on paths where it was never assigned: at O0 that read stack garbage, at O2 the undef read became a trap (j06_json_roundtrip). - BasicBlockHelper::sealOpenBlock emits abort() before `unreachable`: a dangling block is a lowering gap, not a proof the path is dead, and a bare `unreachable` lets the optimizer delete the paths into it. Gates: make check OK. Parallel AOT differential sweep (stdout+rc, 3 runs per case, 386 cases) vs pristine master: 2 cases fixed (discarded_pathinfo_parse_url_elision_36386, discarded_str_pad_split_elision_36386), remaining regressions are only the 9 pow_hundred*_smul cases whose ~20k-line tables crash LLVM at O2 — removed by the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o-throw analysis
pow / ** (php-src Zend/zend_operators.c pow_function_base):
- New PowLongLlvm (__phpc_pow_long): square-and-multiply with overflow
checks and php-src's exact double continuation on overflow; negative
exponent -> double. Replaces ~146k lines of per-exponent chained-multiply
tables (exponents 0..111, eleven files plus a fold trait) whose fallback
for every other exponent was (int)pow((double)b,(double)e): wrong above
2^53 and on overflow (2**64, 3**40, 7**23 ... returned garbage ints where
php-src returns floats). The tables also crashed LLVM at O2.
- JitValueNumeric::powValueOperands (boxed operands) uses it too.
- VmPowNumericOperandGuard: the numeric-string check misclassified "2" as
non-numeric and its continue block was left unterminated (sealed
`unreachable`), so any string operand of ** was UB; the TypeError was a
raw abort(), now the catchable path.
Numeric strings in boxed arithmetic (+ - * / % **):
- New NumericStringLlvm (__phpc_numeric_str) implementing
_is_numeric_string_ex: whitespace, sign, digits, fraction, exponent,
trailing whitespace; integer strings that fit zend_long are IS_LONG.
- "2" + "3" is int(5) (was float(5)); "5 apples" + 1 warns "A non-numeric
value encountered"; "abc" + 1 throws "Unsupported operand types: string +
int" with runtime type names on both sides.
TypeError messages: object-operand errors name the other operand's runtime
type ("Foo + int", was "Foo + mixed").
No-throw call elision (#36386 perf work) was a deny-list (throw/new/
include/calls only), so f($a,$b){return $a+$b;} counted as non-throwing and
the caller skipped its pending-exception check: a TypeError from f()
surfaced only after the caller's next statement (var_dump(f(...)) printed
NULL first). Now an allow-list with operand-type conditions (arithmetic only
on inferred numeric operands, / % only with a non-zero literal divisor,
shifts only with a non-negative literal count). Typed recursion (fibo,
call-heavy) keeps the elision — timings unchanged.
var_dump/echo of a negative NaN printed "-nan" (glibc) — now NAN via fcmp uno.
Bookkeeping: spine smoke list, size budgets, bootstrap inventory/profile,
OWNERSHIP regenerated for the removed/added files (spine 8588/8588).
Gates: make check OK; all 108 pow_*_smul differential cases match Zend
(3 runs each); targeted Zend-vs-AOT scripts for pow (24 int cases incl.
overflow), numeric strings, cross-function TypeErrors all match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three commits (see each message for detail):
OPT_LEVEL>=1crashed (php-llvm signature), default path skipped IR opts + OptNone codegen. Internalize + DCE unreachable runtime, O2 by default, inliner hygiene, delref fast/slow split, per-class TypeError chains → one runtime-name raise (ClassNameTable), compile-cache key now covers all lib/ext sources.unreachable.int ** int(PowLongLlvm, replaces ~146k lines of per-exponent tables with wrong fallback), numeric strings in boxed arithmetic (NumericStringLlvm,"2"+"3"is int), runtime type names in operand TypeErrors, sound no-throw call analysis (callee TypeErrors were caught one statement late),-nan→NAN.Measured (lab, scaled benchmarks, Zend 8.2 pinned image, best of 3, outputs identical)
Geomean AOT/Zend over 10 programs: 3.5× slower → 1.3× slower. untyped loop 2.22s→0.54s (Zend 0.48), nbody 1.29s→0.41s (Zend 0.41), spectral-norm 0.65× Zend, call-heavy 0.23× Zend. Not a committed table (benchmarks/v2 programs run ~10 ms and measure startup; regenerating tables is follow-up).
Gates run
make check(dev-verify-fast) OK on final tree.script/differential-sweep.sh --aot --repeat 3): master 372 ok / 10 fail → this branch 374 ok / 8 fail, no case regressed; fixed:discarded_pathinfo_parse_url_elision_36386,discarded_str_pad_split_elision_36386.north-star5-verify --strict.🤖 Generated with Claude Code