optimize non-native integer hot paths

master
韩天峰 1 day ago
parent 20f0b5a284
commit 10f16726e0
  1. 15
      phpunit/src/SsaAnalysisTest.php
  2. 51
      src/Optimizer/LoopVarOptimizer.php
  3. 12
      src/Parser/AssignOpTrait.php
  4. 5
      src/Parser/BinaryOpTrait.php
  5. 10
      src/Translator.php
  6. 18
      tests/compiler/operator/local-var-compound-overflow.phpt
  7. 18
      tests/compiler/optimizations/loop-var-overflow-semantics.phpt

@ -1199,7 +1199,7 @@ class SsaAnalysisTest extends TestCase
$this->assertArrayNotHasKey('i', $locals);
}
public function testLoopVarOptimizerRejectsUnsafeUseAfterLoop(): void
public function testLoopVarOptimizerAllowsCheckedArithmeticAfterLoop(): void
{
$locals = $this->optimizeLoopVarsForCode('
$n = 10;
@ -1209,6 +1209,19 @@ class SsaAnalysisTest extends TestCase
$x = $i + PHP_INT_MAX;
');
$this->assertSame(Type::INT, $locals['i'] ?? null);
}
public function testLoopVarOptimizerRejectsMutatingUseAfterLoop(): void
{
$locals = $this->optimizeLoopVarsForCode('
$n = 10;
for ($i = 0; $i < $n; $i++) {
echo $i;
}
$i += PHP_INT_MAX;
');
$this->assertArrayNotHasKey('i', $locals);
}

@ -30,14 +30,18 @@ trait LoopVarOptimizer
'mb_substr_count' => true,
];
protected function optimizeLoopVars(SsaBuilder $ssa): void
/**
* @return array<string, string> Escaped local name => narrowed C++ type.
*/
protected function optimizeLoopVars(SsaBuilder $ssa): array
{
$stmts = $ssa->getStmts();
if (!$stmts) {
return;
return [];
}
$candidates = [];
$optimized = [];
$this->collectLoopVarCandidates($stmts, [], $candidates);
foreach ($candidates as $varName => $candidate) {
@ -65,7 +69,10 @@ trait LoopVarOptimizer
continue;
}
$this->context->localVars[$escapedName] = Type::INT;
$optimized[$escapedName] = Type::INT;
}
return $optimized;
}
/**
@ -363,7 +370,24 @@ trait LoopVarOptimizer
}
if ($expr instanceof Expr\Variable && is_string($expr->name)) {
return $safeVars[$expr->name] ?? null;
if (isset($safeVars[$expr->name])) {
return $safeVars[$expr->name];
}
// Typed parameters already use a native php::Int slot. They are
// safe as exclusive loop bounds: `for ($i = 0; $i < $n; $i++)`
// cannot increment past PHP_INT_MAX. Their sign and distance from
// the integer limits are unknown, so do not accept them for the
// non-negative post-decrement or inclusive-bound cases.
$argumentName = $this->escapeVarName($expr->name);
if (($this->context->arguments[$argumentName] ?? null) === Type::INT) {
return [
'nonNegative' => false,
'inclusiveSafe' => false,
];
}
return null;
}
if ($this->isLoopIntCall($expr)) {
@ -388,6 +412,17 @@ trait LoopVarOptimizer
return false;
}
// This pass runs before statement conversion has populated all local
// variable types. Avoid asking the general expression detector to
// resolve chained/dynamic receivers here: apart from being needlessly
// expensive for unrelated assignments, that can report an undefined
// receiver before its preceding assignment has been converted. Direct
// calls such as `$object->toInt()` remain eligible.
if (($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall)
&& (!$expr->var instanceof Expr\Variable || !is_string($expr->var->name))) {
return false;
}
return $this->detectTypeOfExpr($expr) === Type::INT;
}
@ -652,7 +687,15 @@ trait LoopVarOptimizer
}
if ($expr instanceof Expr\BinaryOp) {
if (isset(self::SAFE_INT_BINARY_OPS[$expr->getType()])) {
if (isset(self::SAFE_INT_BINARY_OPS[$expr->getType()])
|| $expr instanceof Expr\BinaryOp\Plus
|| $expr instanceof Expr\BinaryOp\Minus
|| $expr instanceof Expr\BinaryOp\Mul) {
// In ordinary PHP mode BinaryOpTrait routes native Int
// addition/subtraction/multiplication through php::Var's
// checked operators, preserving overflow-to-float semantics.
// Narrowing the loop counter therefore does not make these
// read-only expression uses native C++ arithmetic.
return $this->loopExprHasIntHazard($expr->left, $varName, $allowedNodes)
|| $this->loopExprHasIntHazard($expr->right, $varName, $allowedNodes);
}

@ -917,7 +917,14 @@ trait AssignOpTrait
return $this->parseBigAssignOp($node, $var, $type, $expr, $rightType, $op);
}
$rightExprStr = $this->convertExprType($expr, $type, $rightType);
// A dynamic local must retain the RHS runtime type. Variant's
// compound operators already implement PHP coercion and checked
// integer overflow; eagerly converting an int-looking expression
// both adds an unnecessary unbox/rebox pair and can collapse an
// overflowed float back to int before the assignment.
$rightExprStr = $type === Type::VAR
? $expr
: $this->convertExprType($expr, $type, $rightType);
if ($this->isAssignOpConcat($op)) {
if ($this->isArrayVar($node->var)) {
$this->fatalError($node->var, 'Cannot concat string to array');
@ -933,6 +940,9 @@ trait AssignOpTrait
$powExpr = 'php::fn::pow(' . $var . ', ' . $rightExprStr . ')';
return $var . ' = ' . $this->convertVarType($var, $powExpr);
}
if ($type === Type::VAR && $op === '+=') {
return $var . '.addAssign(' . $rightExprStr . ')';
}
return $var . ' ' . $op . ' ' . $rightExprStr;
}

@ -169,7 +169,10 @@ trait BinaryOpTrait
&& in_array($op, ['+', '-', '*'], true)
&& $this->evaluateConstantIntArithmetic($left, $right, $op) === null
) {
return '((php::Var(' . $leftExpr . ')) ' . $op . ' (php::Var(' . $rightExpr . ')))';
// Keep the potentially widening result boxed, but pass the native
// RHS directly so PHPX can use its inline checked-int overload
// without constructing and destroying another temporary zval.
return '((php::Var(' . $leftExpr . ')) ' . $op . ' (' . $rightExpr . '))';
}
return '((' . $leftExpr . ') ' . $op . ' (' . $rightExpr . '))';

@ -4435,11 +4435,14 @@ CODE;
$ssaBuilder->build();
$this->context->ssaBuilder = $ssaBuilder;
$this->analyzeStableObjects($ssaBuilder);
// Range-proven loop counters are safe to narrow even without
// `use native_types`: the optimizer rejects counters whose PHP
// integer semantics could widen to float or otherwise escape.
$optimizedLoopVars = $this->optimizeLoopVars($ssaBuilder);
if ($this->nativeTypes) {
// Narrow local variable types based on SSA analysis.
$this->optimizeVarTypes($ssaBuilder);
// Narrow range-proven loop counters and native property accesses.
$this->optimizeLoopVars($ssaBuilder);
// Narrow native property accesses.
$this->optimizeObjectProps($ssaBuilder);
}
$this->context->resetAnalysisTemporaries(
@ -4449,6 +4452,9 @@ CODE;
$oriNativeObjects,
$oriNonNullNativeObjects,
);
foreach ($optimizedLoopVars as $varName => $type) {
$this->context->localVars[$varName] = $type;
}
}
$stmts = '';

@ -0,0 +1,18 @@
--TEST--
Dynamic local compound assignment preserves PHP integer overflow promotion
--FILE--
<?php
function main(): void {
$add = PHP_INT_MAX;
$add += 1;
$mul = PHP_INT_MAX;
$mul *= 2;
var_dump(is_float($add));
var_dump(is_float($mul));
}
?>
--EXPECT--
bool(true)
bool(true)

@ -0,0 +1,18 @@
--TEST--
Loop var optimizer preserves PHP overflow-to-float semantics without native_types
--FILE--
<?php
function main(): void {
$value = 0;
for ($i = 9223372036854775806; $i < 9223372036854775807; $i++) {
$value = $i + 2;
}
var_dump($i);
var_dump(is_float($value));
}
?>
--EXPECT--
int(9223372036854775807)
bool(true)
Loading…
Cancel
Save