From 6df71ec55768c050f3699b0526a788f0a8a92bc8 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 28 Aug 2026 16:13:11 +0800 Subject: [PATCH] perf: optimize stable integer property sums --- benchmark/property-access/README.md | 5 +- benchmark/property-access/run.php | 30 +++++- src/CompilerBase.php | 9 ++ src/Parser/BinaryOpTrait.php | 101 +++++++++++++++++- .../operator/runtime-int-overflow-return.phpt | 58 ++++++++++ .../final-int-property-add-chain.phpt | 48 +++++++++ 6 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 tests/compiler/operator/runtime-int-overflow-return.phpt create mode 100644 tests/compiler/optimizations/final-int-property-add-chain.phpt diff --git a/benchmark/property-access/README.md b/benchmark/property-access/README.md index 64e26f2e..488dc99e 100644 --- a/benchmark/property-access/README.md +++ b/benchmark/property-access/README.md @@ -1,8 +1,9 @@ # Dynamic property benchmark This benchmark compares the same dynamic and static property operations under -Zend PHP and a TypePHP `-O2` binary. Each metric is the best of seven rounds -after three warm-up rounds and is reported in nanoseconds per property access. +Zend PHP and a TypePHP `-O3` + LTO binary. Each metric is the best of seven +rounds after three warm-up rounds and is reported in nanoseconds per property +access. Run it from the repository root: diff --git a/benchmark/property-access/run.php b/benchmark/property-access/run.php index ce08f0e1..5776accd 100644 --- a/benchmark/property-access/run.php +++ b/benchmark/property-access/run.php @@ -14,12 +14,22 @@ foreach ($argv as $argument) { } } -/** @param list $command */ -function runCommand(array $command, string $cwd, bool $capture): string +/** + * @param list $command + * @param array|null $environment + */ +function runCommand(array $command, string $cwd, bool $capture, ?array $environment = null): string { $stdout = $capture ? ['pipe', 'w'] : STDOUT; $stderr = $capture ? ['pipe', 'w'] : STDERR; - $process = proc_open($command, [STDIN, $stdout, $stderr], $pipes, $cwd, null, ['bypass_shell' => true]); + $process = proc_open( + $command, + [STDIN, $stdout, $stderr], + $pipes, + $cwd, + $environment, + ['bypass_shell' => true], + ); if (!is_resource($process)) { throw new RuntimeException('Failed to start: ' . implode(' ', $command)); } @@ -79,7 +89,19 @@ $php = parseResults(runCommand([ '-r', 'require ' . var_export($source, true) . '; main();', ], $root, true)); -$typephp = parseResults(runCommand([$binary], $root, true)); +$typephpEnvironment = null; +if (PHP_OS_FAMILY !== 'Windows') { + $phpxHome = getenv('PHPX_HOME'); + if (!is_string($phpxHome) || $phpxHome === '') { + $phpxHome = $root . '/vendor/swoole/phpx'; + } + $typephpEnvironment = getenv(); + $loaderVariable = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH'; + $existingPath = $typephpEnvironment[$loaderVariable] ?? ''; + $typephpEnvironment[$loaderVariable] = $phpxHome . '/lib' + . ($existingPath === '' ? '' : PATH_SEPARATOR . $existingPath); +} +$typephp = parseResults(runCommand([$binary], $root, true, $typephpEnvironment)); echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\n"; echo "------------------------------------------------------------\n"; diff --git a/src/CompilerBase.php b/src/CompilerBase.php index b8a36550..94a2e947 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -2390,6 +2390,15 @@ class CompilerBase implements PropertyAccessContext } // 实际函数的返回值 $type = $this->detectTypeOfExpr($v->expr); + // In ordinary PHP mode, int +/−/* int is only conditionally an int: + // runtime overflow promotes the result to float. Keep the Variant + // representation through the return boundary so a declared scalar + // return type observes and rejects that float exactly as PHP does. + // `use native_types` intentionally opts into native C++ arithmetic + // semantics and is therefore excluded from this check. + if (!$this->nativeTypes && $type === Type::INT && $this->exprCanOverflowInt($v->expr)) { + $type = Type::VAR; + } $nativeExpressionClass = $this->detectClassOfExpr($v->expr); if ($this->context->inClosure && $this->isNativeObjectClass($nativeExpressionClass)) { $this->fatalError($v, 'Zend closures cannot return native objects'); diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 804cfb2e..3212cb40 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -15,6 +15,7 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp; use PhpParser\NodeAbstract; +use PhpParser\Modifiers; trait BinaryOpTrait { @@ -154,6 +155,21 @@ trait BinaryOpTrait return $folded; } + // Declared int parameters use the native Int ABI even in ordinary PHP + // mode. A direct C++ +/−/* would therefore have undefined signed + // overflow, while PHP promotes the result to float. Route dynamic + // integer arithmetic through the encapsulated Variant operators unless + // the user explicitly selected `use native_types`. Fully constant + // expressions remain safe to emit directly after the checks above. + if (!$this->nativeTypes + && $leftType === Type::INT + && $rightType === Type::INT + && in_array($op, ['+', '-', '*'], true) + && $this->evaluateConstantIntArithmetic($left, $right, $op) === null + ) { + return '((php::Var(' . $leftExpr . ')) ' . $op . ' (php::Var(' . $rightExpr . ')))'; + } + return '((' . $leftExpr . ') ' . $op . ' (' . $rightExpr . '))'; } @@ -641,10 +657,93 @@ trait BinaryOpTrait protected function parseBinaryOpPlus(Expr\BinaryOp\Plus $expr): string { - return $this->parsePythonBinaryOperator($expr) + $python = $this->parsePythonBinaryOperator($expr); + if ($python !== null) { + return $python; + } + + return $this->tryParseFinalIntPropertyAddChain($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '+'); } + /** + * Lower a left-associated chain of stable declared-int property reads into + * one detached Variant accumulator. + * + * This keeps PHP overflow promotion and evaluation order in Variant's + * encapsulated operator+= while avoiding one owning temporary per binary + * AST node. The class/property must be final so a subclass cannot replace + * the declared property with a hook. Nullable, virtual and hooked + * properties stay on the general path. + */ + protected function tryParseFinalIntPropertyAddChain(Expr\BinaryOp\Plus $expr): ?string + { + if ($this->nativeTypes) { + return null; + } + + $operands = []; + $cursor = $expr; + while ($cursor instanceof Expr\BinaryOp\Plus) { + array_unshift($operands, $cursor->right); + $cursor = $cursor->left; + } + array_unshift($operands, $cursor); + + if (count($operands) < 3) { + return null; + } + + foreach ($operands as $operand) { + if (!$this->isStableFinalIntPropertyRead($operand)) { + return null; + } + } + + $accumulator = $this->addTmpVar(Type::VAR); + foreach ($operands as $index => $operand) { + /** @var Expr\PropertyFetch $operand */ + $value = $this->parsePropertyFetch($operand); + if ($index === 0) { + // Assignment into an already-declared Variant materializes an + // independent value. Do not use copy-initialization here: + // mandatory C++ copy elision could retain an Indirect alias. + $this->context->beforeStmtLines[] = $accumulator . ' = ' . $value . ';'; + } else { + $this->context->beforeStmtLines[] = $accumulator . ' += ' . $value . ';'; + } + } + + return $accumulator; + } + + protected function isStableFinalIntPropertyRead(NodeAbstract $operand): bool + { + if (!$operand instanceof Expr\PropertyFetch + || !$operand->var instanceof Expr\Variable + || !$this->isIdExpr($operand->name) + ) { + return false; + } + + $class = $this->resolveObjectClassDef($operand->var); + $propertyName = $this->parseIdentifier($operand->name); + if ($class === null || !$class->hasProperty($propertyName)) { + return false; + } + + $property = $class->getProperty($propertyName); + $stableDeclaration = ($class->flags & Modifiers::FINAL) !== 0 + || ($property->flags & Modifiers::FINAL) !== 0; + + return $stableDeclaration + && ($property->flags & Modifiers::STATIC) === 0 + && $property->type === Type::INT + && !$property->nullable + && !$property->virtual + && $property->getter === null; + } + protected function parseBinaryOpMul(Expr\BinaryOp\Mul $expr): string { return $this->parsePythonBinaryOperator($expr) diff --git a/tests/compiler/operator/runtime-int-overflow-return.phpt b/tests/compiler/operator/runtime-int-overflow-return.phpt new file mode 100644 index 00000000..55a5404c --- /dev/null +++ b/tests/compiler/operator/runtime-int-overflow-return.phpt @@ -0,0 +1,58 @@ +--TEST-- +Runtime integer overflow is checked at an int return boundary +--FILE-- +left + $this->right; + } +} + +function addInts(int $left, int $right): int +{ + return $left + $right; +} + +function subtractInts(int $left, int $right): int +{ + return $left - $right; +} + +function multiplyInts(int $left, int $right): int +{ + return $left * $right; +} + +function main(): void +{ + foreach ([ + static fn (): int => addInts(PHP_INT_MAX, 1), + static fn (): int => subtractInts(PHP_INT_MIN, 1), + static fn (): int => multiplyInts(PHP_INT_MAX, 2), + static function (): int { + $value = new OverflowProperties(); + $value->left = PHP_INT_MAX; + $value->right = 1; + return $value->sum(); + }, + ] as $callback) { + try { + var_dump($callback()); + } catch (TypeError $error) { + echo $error->getMessage(), "\n"; + } + } +} +?> +--EXPECTF-- +addInts(): Return value must be of type int, float returned +subtractInts(): Return value must be of type int, float returned +multiplyInts(): Return value must be of type int, float returned +OverflowProperties::sum(): Return value must be of type int, float returned diff --git a/tests/compiler/optimizations/final-int-property-add-chain.phpt b/tests/compiler/optimizations/final-int-property-add-chain.phpt new file mode 100644 index 00000000..2371507e --- /dev/null +++ b/tests/compiler/optimizations/final-int-property-add-chain.phpt @@ -0,0 +1,48 @@ +--TEST-- +Final int property addition uses a detached value accumulator +--FILE-- +first + $this->second + $this->third + $this->fourth + $this->fifth; + } +} + +function main(): void +{ + $value = new AddChain(); + $first =& $value->first; + + var_dump($value->sum()); + var_dump($value->first, $first); + + $value->first = PHP_INT_MAX; + $value->second = 1; + $value->third = 0; + $value->fourth = 0; + $value->fifth = 0; + try { + var_dump($value->sum()); + } catch (TypeError $error) { + echo $error->getMessage(), "\n"; + } + var_dump($value->first, $first); +} +?> +--EXPECTF-- +int(15) +int(1) +int(1) +AddChain::sum(): Return value must be of type int, float returned +int(9223372036854775807) +int(9223372036854775807)