perf: optimize stable integer property sums

master^2
韩天峰 6 hours ago
parent 0821f62f20
commit a4dee96fcf
  1. 5
      benchmark/property-access/README.md
  2. 30
      benchmark/property-access/run.php
  3. 9
      src/CompilerBase.php
  4. 101
      src/Parser/BinaryOpTrait.php
  5. 58
      tests/compiler/operator/runtime-int-overflow-return.phpt
  6. 48
      tests/compiler/optimizations/final-int-property-add-chain.phpt

@ -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:

@ -14,12 +14,22 @@ foreach ($argv as $argument) {
}
}
/** @param list<string> $command */
function runCommand(array $command, string $cwd, bool $capture): string
/**
* @param list<string> $command
* @param array<string, string>|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";

@ -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');

@ -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)

@ -0,0 +1,58 @@
--TEST--
Runtime integer overflow is checked at an int return boundary
--FILE--
<?php
declare(strict_types=1);
final class OverflowProperties
{
public int $left = 0;
public int $right = 0;
public function sum(): int
{
return $this->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

@ -0,0 +1,48 @@
--TEST--
Final int property addition uses a detached value accumulator
--FILE--
<?php
declare(strict_types=1);
final class AddChain
{
public int $first = 1;
public int $second = 2;
public int $third = 3;
public int $fourth = 4;
public int $fifth = 5;
public function sum(): int
{
return $this->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)
Loading…
Cancel
Save