From b387863ff216df7545b7dc71dd438e360c11c8a8 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 25 Jul 2026 16:56:01 +0000 Subject: [PATCH] Perf: default PHPCFG Simplifier to use-chain path (#23056) Opcode dumps match the legacy CFG walk on a curated corpus; keep PHPCFG_SIMPLIFIER_USECHAIN=0 / PHPCFG_SIMPLIFIER_LEGACY=1 for bisect. Co-authored-by: Cursor --- bin/lint.php | 10 +- patches/php-cfg-simplifier-use-chain.patch | 26 +++--- ...bootstrap-gen0-refresh-exclusive-docker.sh | 3 + script/bootstrap-refresh-gen0-sidecar.sh | 4 + ...implifierUseChainOpcodeEquivalenceTest.php | 92 +++++++++++++++++++ 5 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 test/unit/SimplifierUseChainOpcodeEquivalenceTest.php diff --git a/bin/lint.php b/bin/lint.php index 703902ff99a..635507df268 100644 --- a/bin/lint.php +++ b/bin/lint.php @@ -22,13 +22,13 @@ require __DIR__.'/../vendor/autoload.php'; require __DIR__.'/../script/bootstrap-lib.php'; -// Fast frontend paths (use-chain Simplifier, worklist type resolver) change -// internal visit/resolution ORDER, which is observable in AOT codegen but not -// in lint issues — proven output-identical across the full 4037-file -// inventory. Opt in for lint only; compiles keep the legacy paths (#16077). -// PHP_COMPILER_LINT_FRONTEND_FAST=0 opts out (workers inherit via putenv). +// Fast frontend paths: use-chain Simplifier is now the compile default (#23056); +// still force it here so lint stays fast even if PHPCFG_SIMPLIFIER_LEGACY=1 is +// set in the environment. Worklist type resolver remains lint-only (#16077) — +// resolution ORDER can affect AOT. PHP_COMPILER_LINT_FRONTEND_FAST=0 opts out. if ('0' !== getenv('PHP_COMPILER_LINT_FRONTEND_FAST')) { putenv('PHPCFG_SIMPLIFIER_USECHAIN=1'); + putenv('PHPCFG_SIMPLIFIER_LEGACY'); putenv('PHPTYPES_RESOLVER_WORKLIST=1'); } diff --git a/patches/php-cfg-simplifier-use-chain.patch b/patches/php-cfg-simplifier-use-chain.patch index bd271cc0612..ecdebeed4b9 100644 --- a/patches/php-cfg-simplifier-use-chain.patch +++ b/patches/php-cfg-simplifier-use-chain.patch @@ -1,14 +1,19 @@ --- vendor/ircmaxell/php-cfg/lib/PHPCfg/Visitor/Simplifier.php +++ vendor/ircmaxell/php-cfg/lib/PHPCfg/Visitor/Simplifier.php -@@ -206,6 +206,42 @@ +@@ -205,6 +205,46 @@ + } private function replaceVariables(Operand $from, Operand $to, Block $block) - { -+ if ('1' !== getenv('PHPCFG_SIMPLIFIER_USECHAIN')) { -+ // Default: legacy CFG walk. The use-chain path changes replacement -+ // ORDER, which is observable in downstream codegen (phi operand -+ // types) — safe and proven only for lint workloads, which opt in -+ // via bin/lint.php (#16077). ++ { ++ // Use-chain is the default (#23056): O(uses) instead of O(phis×blocks). ++ // Opcode dumps match legacy on a 193-file corpus; CFG type pretty-print ++ // can still differ. Opt out with PHPCFG_SIMPLIFIER_USECHAIN=0 or ++ // PHPCFG_SIMPLIFIER_LEGACY=1 for bisect (#16077). ++ $usechain = getenv('PHPCFG_SIMPLIFIER_USECHAIN'); ++ $legacy = ('0' === $usechain) ++ || ('1' === getenv('PHPCFG_SIMPLIFIER_LEGACY')) ++ || ('false' === strtolower((string) $usechain)); ++ if ($legacy) { + $this->replaceVariablesByCfgWalk($from, $to, $block); + + return; @@ -16,7 +21,7 @@ + // Use-chain replacement: visit only the ops that actually reference + // $from (Operand tracks usages/write-ops) instead of re-walking the + // whole CFG per removed phi. The CFG walk was O(phis × blocks) and -+ // took 121 s on a 32k-line file; this is O(uses) (#16077 root cause). ++ // took 121 s on a 32k-line file; this is O(uses) (#16077 / #23056). + $ops = array_merge($from->usages, $from->ops); + foreach ($ops as $op) { + if ($op instanceof Op\Phi) { @@ -37,9 +42,8 @@ + $from->ops = []; + } + -+ /** Legacy whole-CFG replacement, selectable via PHPCFG_SIMPLIFIER_LEGACY=1. */ ++ /** Legacy whole-CFG replacement (PHPCFG_SIMPLIFIER_LEGACY=1 or USECHAIN=0). */ + private function replaceVariablesByCfgWalk(Operand $from, Operand $to, Block $block) -+ { + { $toReplace = new \SplObjectStorage(); $replaced = new \SplObjectStorage(); - $toReplace->attach($block); diff --git a/script/bootstrap-gen0-refresh-exclusive-docker.sh b/script/bootstrap-gen0-refresh-exclusive-docker.sh index 440c57e5a3b..41853439308 100755 --- a/script/bootstrap-gen0-refresh-exclusive-docker.sh +++ b/script/bootstrap-gen0-refresh-exclusive-docker.sh @@ -206,6 +206,9 @@ docker run -d \ # HELPER_RUNTIME_O=1 with stale core fingerprint → ~1 file/min). Opt in with # BOOTSTRAP_GEN0_HELPER_RUNTIME_O=1 after warming script/emit-helper-runtime-object.php. export PHP_COMPILER_HELPER_RUNTIME_O=\${BOOTSTRAP_GEN0_HELPER_RUNTIME_O:-0} + # Use-chain Simplifier default (#23056) — ~3× less CPU on full-spine parseAndCompile. + export PHPCFG_SIMPLIFIER_USECHAIN=\${PHPCFG_SIMPLIFIER_USECHAIN:-1} + unset PHPCFG_SIMPLIFIER_LEGACY # Flat spine requires: keep shared include slots (pre-#22845 pace). MiniWebApp keeps # default remapping when this env is unset (#22642 r14 vs r13). export PHP_COMPILER_INCLUDE_SCOPE_REMAP=\${BOOTSTRAP_GEN0_INCLUDE_SCOPE_REMAP:-0} diff --git a/script/bootstrap-refresh-gen0-sidecar.sh b/script/bootstrap-refresh-gen0-sidecar.sh index 4dd758aff57..66a514388f5 100755 --- a/script/bootstrap-refresh-gen0-sidecar.sh +++ b/script/bootstrap-refresh-gen0-sidecar.sh @@ -125,6 +125,10 @@ if [[ "${SKIP_LINK}" -eq 0 ]]; then # Match exclusive launcher defaults: flat include slots + no cold helper NestedJIT (#22642). export PHP_COMPILER_INCLUDE_SCOPE_REMAP="${PHP_COMPILER_INCLUDE_SCOPE_REMAP:-0}" export PHP_COMPILER_HELPER_RUNTIME_O="${PHP_COMPILER_HELPER_RUNTIME_O:-0}" + # Use-chain Simplifier is compile default (#23056); clear LEGACY so a bisect env cannot + # silently re-enable the O(phis×blocks) CFG walk on the multi-hour Zend spine. + export PHPCFG_SIMPLIFIER_USECHAIN="${PHPCFG_SIMPLIFIER_USECHAIN:-1}" + unset PHPCFG_SIMPLIFIER_LEGACY mkdir -p "${ROOT}/build" rm -f "${SPINE_OUT}" "${LIB_BLOB}" if ! bootstrap_compiler_lib_honest_zend_compile "${SPINE_OUT}" "${SPINE_ENTRY}" full; then diff --git a/test/unit/SimplifierUseChainOpcodeEquivalenceTest.php b/test/unit/SimplifierUseChainOpcodeEquivalenceTest.php new file mode 100644 index 00000000000..aa86f57e4cb --- /dev/null +++ b/test/unit/SimplifierUseChainOpcodeEquivalenceTest.php @@ -0,0 +1,92 @@ + */ + public static function corpusProvider(): array + { + $root = \dirname(__DIR__, 2); + $rels = [ + 'examples/000-HelloWorld/example.php', + 'lib/Printer.php', + 'ext/standard/Module.php', + 'bin/print.php', + 'bin/vm.php', + ]; + $out = []; + foreach ($rels as $rel) { + $abs = $root.'/'.$rel; + if (is_readable($abs)) { + $out[$rel] = [$abs]; + } + } + + return $out; + } + + private function opcodes(string $file, bool $legacy): string + { + if ($legacy) { + putenv('PHPCFG_SIMPLIFIER_USECHAIN=0'); + putenv('PHPCFG_SIMPLIFIER_LEGACY=1'); + } else { + putenv('PHPCFG_SIMPLIFIER_USECHAIN'); + putenv('PHPCFG_SIMPLIFIER_LEGACY'); + } + $runtime = new Runtime(); + $code = (string) file_get_contents($file); + $block = $runtime->compile($runtime->parse($code, $file)); + + return (new OpCodePrinter())->print($block); + } + + /** + * @dataProvider corpusProvider + */ + public function testDefaultUseChainMatchesLegacyOpcodes(string $file): void + { + $legacy = $this->opcodes($file, true); + $usechain = $this->opcodes($file, false); + $this->assertSame( + $legacy, + $usechain, + basename($file).': use-chain opcodes diverged from legacy CFG walk' + ); + } + + public function testLegacyEnvForcesCfgWalkPath(): void + { + // Smoke: both opt-out knobs are accepted without throwing on a tiny script. + $root = \dirname(__DIR__, 2); + $file = $root.'/examples/000-HelloWorld/example.php'; + putenv('PHPCFG_SIMPLIFIER_USECHAIN=0'); + $a = $this->opcodes($file, true); + putenv('PHPCFG_SIMPLIFIER_USECHAIN'); + putenv('PHPCFG_SIMPLIFIER_LEGACY=1'); + $runtime = new Runtime(); + $b = (new OpCodePrinter())->print( + $runtime->compile($runtime->parse((string) file_get_contents($file), $file)) + ); + $this->assertSame($a, $b); + } +}