feat(php): 添加SSA对象属性引用提升优化器

- 引入SsaPropOptimizer trait实现对象属性引用提升功能
- 扩展CompilerBase类集成新的SSA属性优化器
- 实现对象稳定性分析和安全属性访问检测
- 支持对SSA证明稳定的对象进行int/float属性提升
- 添加对魔术方法、引用捕获等危险操作的安全检查
- 为稳定对象属性生成C++引用别名提升性能
- 添加相关单元测试验证优化效果
pull/1/head
韩天峰 3 months ago
parent bf31da8e7b
commit a4a869b7cd
  1. 30
      src/Php/CompilerBase.php
  2. 8
      src/Php/Context/FunctionContext.php
  3. 401
      src/Php/Optimizer/SsaPropOptimizer.php
  4. 20
      tests/aot/optimizations/object-int-prop.phpt
  5. 21
      tests/aot/optimizations/objprop-hoist-float.phpt
  6. 21
      tests/aot/optimizations/objprop-hoist-int.phpt
  7. 23
      tests/aot/optimizations/objprop-hoist-magic.phpt
  8. 23
      tests/aot/optimizations/objprop-hoist-multi-def.phpt
  9. 24
      tests/aot/optimizations/objprop-hoist-ref-capture.phpt

@ -29,6 +29,7 @@ use PhpAot\Php\Generator\PlaceHolderGenerator;
use PhpAot\Php\Generator\PropertyPromotion;
use PhpAot\Php\Generator\Utils;
use PhpAot\Php\Generator\TypeCheckGenerator;
use PhpAot\Php\Optimizer\SsaPropOptimizer;
use PhpAot\Php\Optimizer\SsaTypeOptimizer;
use PhpAot\Php\Parser\StdContainerTrait;
use PhpAot\Php\Parser\AssignOpTrait;
@ -75,6 +76,7 @@ class CompilerBase extends \PhpAot\Core\Translator
use Utils;
use TypeCheckGenerator;
use SsaTypeOptimizer;
use SsaPropOptimizer;
public const string TYPE_VAR = 'php::Var';
public const string TYPE_BOOL = 'php::Bool';
@ -1151,6 +1153,8 @@ class CompilerBase extends \PhpAot\Core\Translator
$this->context->ssaBuilder->build();
// Narrow local variable types based on SSA analysis
$this->optimizeVarTypes();
// Analyze object stability for property reference hoisting
$this->optimizeObjectProps();
}
$stmts = '';
@ -3755,23 +3759,29 @@ class CompilerBase extends \PhpAot\Core\Translator
$id = $this->getPropertyIdentifier($expr, $object, $property);
$objectVar = $this->parseIdentifier($object);
$getProperty = $objectVar . '.attr(' . $id . ', ' . $this->escapeBool($update) . ')';
// 将属性设置为 native 类型的引用,以提高性能,仅适用于 this_ ,其他类型对象可能会在中间发生改变
// TODO 分析对象是否为 SSA , 如果是 SSA 则可以将属性设置为引用
if ($expr->hasAttribute('nativePropertyDef') and $this->nativeTypes and $objectVar === 'this_') {
if ($expr->hasAttribute('nativePropertyDef') and $this->nativeTypes) {
/**
* @var PropertyDef $def
*/
$def = $expr->getAttribute('nativePropertyDef');
if ($def->type === self::TYPE_INT or $def->type === self::TYPE_FLOAT) {
$propVar = $this->getObjectPropVarName($objectVar, $this->parseIdentifier($property));
if (!$this->hasObjectPropVar($propVar)) {
$this->context->objectProps[$propVar] = [
'type' => $def->type,
'getter' => $getProperty,
];
if ($objectVar === 'this_') {
if (!$this->hasObjectPropVar($propVar)) {
$this->context->objectProps[$propVar] = [
'type' => $def->type,
'getter' => $getProperty,
];
}
$expr->setAttribute('nativePropertyVar', $propVar);
return $propVar;
}
// SSA-stable object: lazily create reference at first access point
if ($this->isStableObject($objectVar)) {
$result = $this->hoistStableObjectProp($objectVar, $this->parseIdentifier($property), $id, $def->type);
$expr->setAttribute('nativePropertyVar', $result);
return $result;
}
$expr->setAttribute('nativePropertyVar', $propVar);
return $propVar;
}
}
return $getProperty;

@ -15,6 +15,12 @@ class FunctionContext
/** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */
public ?SsaBuilder $ssaBuilder = null;
/** Map of SSA-stable object variable name => class name (SsaPropOptimizer). */
public array $stableObjects = [];
/** Map of hoisted property refs: objName => [propName => true] (SsaPropOptimizer). */
public array $hoistedProps = [];
/**
* @var array<string, string>
*/
@ -64,6 +70,8 @@ class FunctionContext
$this->stdArrays = [];
$this->stdContainers = [];
$this->objectProps = [];
$this->stableObjects = [];
$this->hoistedProps = [];
$this->ceWrappers = [];
$this->tmpVarIndex = 0;
$this->scopeLayouts = [];

@ -0,0 +1,401 @@
<?php
/**
* SSA-based object property reference hoisting optimizer.
*
* Extends the existing $this->intProp optimization to any SSA-proven stable
* object. When an object variable has a single definition, no escape/reference/
* kill flags, and its class has no magic methods that intercept property access,
* int/float property accesses can be hoisted to C++ references aliasing the
* zval's internal value slot.
*
* Prerequisites checked by this pass:
* 1. Object has exactly one SSA definition (single assignment)
* 2. No REFERENCE / ESCAPED / KILLED flags on the object's SSA vars
* 3. Class has no __get / __set magic methods
* 4. Property has a declared native type (int or float)
* 5. No unset($o->prop) on the property
* 6. No &$o->prop (reference capture of the property)
* 7. No func(&$o->prop) (property passed by reference)
* 8. First access is not inside a loop or nested block scope
*/
namespace PhpAot\Php\Optimizer;
use PhpAot\Php\Analysis\SsaBuilder;
use PhpAot\Php\Analysis\SsaFlags;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\NodeAbstract;
trait SsaPropOptimizer
{
/**
* Analyze object stability and identify safe property accesses.
* Called after SSA build and var type optimization in parseFunction().
*
* Scans the function body AST to find object assignments (e.g. $o = new Foo()),
* resolves class names, and checks SSA stability for each object variable.
* This must be done during analysis because $this->context->objects is only
* populated during code generation (after analysis).
*/
protected function optimizeObjectProps(): void
{
$ssa = $this->context->ssaBuilder;
if (!$ssa || empty($ssa->ssaVars) || !$this->nativeTypes) {
return;
}
$objectAssigns = $this->collectObjectAssignments($ssa->getStmts());
// Also check function parameters that are typed objects
foreach ($this->context->objects as $objName => $className) {
if ($objName === 'this_') {
continue;
}
$objectAssigns[$objName] = $className;
}
foreach ($objectAssigns as $objName => $className) {
if ($objName === 'this_') {
continue;
}
if (!$className || $className === 'stdClass' || !$this->hasClass($className)) {
continue;
}
if (!$this->isObjectSsaStable($ssa, $objName)) {
continue;
}
if (!$this->isClassSafeForPropHoisting($className)) {
continue;
}
if ($this->hasDangerousPropOps($objName, $ssa->getStmts())) {
continue;
}
$this->context->stableObjects[$objName] = $className;
}
}
/**
* Walk the function body AST to find variable assignments that produce
* typed objects. Returns map of varName => className.
*/
private function collectObjectAssignments(array $stmts): array
{
$result = [];
foreach ($stmts as $stmt) {
$this->scanStmtForObjectAssign($stmt, $result);
}
return $result;
}
private function scanStmtForObjectAssign($stmt, array &$result): void
{
if (!$stmt instanceof Node) {
return;
}
if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\Assign) {
$assign = $stmt->expr;
$var = $assign->var;
if ($var instanceof Expr\Variable && is_string($var->name)) {
$className = $this->resolveNewExprClass($assign->expr);
if ($className) {
$result[$var->name] = $className;
}
}
return;
}
// Recurse
if ($stmt instanceof Node\Stmt\If_) {
foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result);
foreach ($stmt->elseifs as $elseif) {
foreach ($elseif->stmts as $s) $this->scanStmtForObjectAssign($s, $result);
}
if ($stmt->else) {
foreach ($stmt->else->stmts as $s) $this->scanStmtForObjectAssign($s, $result);
}
} elseif ($stmt instanceof Node\Stmt\While_ || $stmt instanceof Node\Stmt\Do_) {
foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result);
} elseif ($stmt instanceof Node\Stmt\For_ || $stmt instanceof Node\Stmt\Foreach_) {
foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result);
} elseif ($stmt instanceof Node\Stmt\TryCatch) {
foreach ($stmt->stmts as $s) $this->scanStmtForObjectAssign($s, $result);
foreach ($stmt->catches as $catch) {
foreach ($catch->stmts as $s) $this->scanStmtForObjectAssign($s, $result);
}
if ($stmt->finally) {
foreach ($stmt->finally->stmts as $s) $this->scanStmtForObjectAssign($s, $result);
}
}
}
/**
* Resolve the class name from a `new ClassName()` expression,
* or a function/method call that returns a known object type.
*/
private function resolveNewExprClass(Expr $expr): ?string
{
if ($expr instanceof Expr\New_) {
if ($expr->class instanceof Node\Name) {
$className = $expr->class->toString();
if ($className === 'self' || $className === 'static') {
if ($this->classDef) {
$className = $this->classDef->getFullName();
} else {
return null;
}
}
return $this->getNamespacedClassName($className);
}
}
// For function/method calls, try to detect the return class
if ($expr instanceof Expr\FuncCall || $expr instanceof Expr\MethodCall
|| $expr instanceof Expr\StaticCall || $expr instanceof Expr\NullsafeMethodCall) {
return $this->detectClassOfExpr($expr) ?: null;
}
return null;
}
/**
* Check if an object variable has a single stable SSA definition.
*/
private function isObjectSsaStable(SsaBuilder $ssa, string $objName): bool
{
$foundDef = false;
foreach ($ssa->ssaVars as $ssaVar) {
if ($ssaVar->origName !== $objName) {
continue;
}
if ($ssaVar->flags & SsaFlags::PHI) {
continue;
}
if ($ssaVar->flags & (SsaFlags::REFERENCE | SsaFlags::ESCAPED | SsaFlags::KILLED)) {
return false;
}
if ($foundDef) {
return false; // Multiple definitions
}
if (!$this->isObjectDefinition($ssaVar)) {
return false;
}
$foundDef = true;
}
return $foundDef;
}
/**
* Check if an SSA definition sets the variable to an object value.
* Accepts both `new ClassName()` and calls that return a typed object.
*/
private function isObjectDefinition($ssaVar): bool
{
$def = $ssaVar->definition;
if (!$def) {
return false;
}
if ($def instanceof Node\Stmt\Expression && $def->expr instanceof Expr\Assign) {
$rhs = $def->expr->expr;
if ($rhs instanceof Expr\New_) {
return true;
}
// Allow function/method calls that return a matching typed object
if ($rhs instanceof Expr\FuncCall || $rhs instanceof Expr\MethodCall
|| $rhs instanceof Expr\StaticCall || $rhs instanceof Expr\NullsafeMethodCall) {
return true;
}
}
return false;
}
/**
* Check if a class has no magic methods that intercept property access.
*/
private function isClassSafeForPropHoisting(string $className): bool
{
$classDef = $this->classes[$this->escapeClass($className)] ?? null;
if (!$classDef) {
return false;
}
if ($classDef->hasMethod('__get') || $classDef->hasMethod('__set')) {
return false;
}
return true;
}
/**
* Scan function body for dangerous operations on object properties.
*
* Detects:
* - unset($o->prop) — destroys property slot
* - $ref = &$o->prop — property becomes reference, zval type changes
* - func(&$o->prop) or $obj->method(&$o->prop) — property passed by ref
*/
private function hasDangerousPropOps(string $objName, array $stmts): bool
{
foreach ($stmts as $stmt) {
if ($this->scanDangerousPropOp($stmt, $objName)) {
return true;
}
}
return false;
}
private function scanDangerousPropOp($stmt, string $objName): bool
{
if (!$stmt instanceof Node) {
return false;
}
// unset($o->prop) — typed property can't be unset in PHP 8,
// but untyped dynamic properties could be. Check anyway.
if ($stmt instanceof Node\Stmt\Unset_) {
foreach ($stmt->vars as $var) {
if ($this->isPropOfObj($var, $objName)) {
return true;
}
}
}
// $ref = &$o->prop — reference capture of property
if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\AssignRef) {
if ($this->isPropOfObj($stmt->expr->expr, $objName)) {
return true;
}
}
// Check function/method call arguments for &$o->prop patterns
if ($stmt instanceof Node\Stmt\Expression) {
$expr = $stmt->expr;
$args = null;
if ($expr instanceof Expr\FuncCall) {
$args = $expr->args;
} elseif ($expr instanceof Expr\MethodCall || $expr instanceof Expr\StaticCall
|| $expr instanceof Expr\NullsafeMethodCall) {
$args = $expr->args;
}
if ($args) {
foreach ($args as $arg) {
// Explicit &$o->prop
if ($arg->byRef && $this->isPropOfObj($arg->value, $objName)) {
return true;
}
// refval($o->prop) pseudo-function
if ($arg->value instanceof Expr\FuncCall
&& $arg->value->name instanceof Node\Name
&& $arg->value->name->toLowerString() === 'refval'
&& !empty($arg->value->args)) {
$inner = $arg->value->args[0]->value;
if ($this->isPropOfObj($inner, $objName)) {
return true;
}
}
}
}
}
// Recurse into compound statements
return $this->recurseDangerousPropOp($stmt, $objName);
}
/**
* Check if an expression is a property fetch on a specific object.
*/
private function isPropOfObj($node, string $objName): bool
{
return $node instanceof Expr\PropertyFetch
&& $node->var instanceof Expr\Variable
&& is_string($node->var->name)
&& $node->var->name === $objName;
}
private function recurseDangerousPropOp($stmt, string $objName): bool
{
if ($stmt instanceof Node\Stmt\If_) {
if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true;
foreach ($stmt->elseifs as $elseif) {
if ($this->hasDangerousPropOps($objName, $elseif->stmts)) return true;
}
if ($stmt->else && $this->hasDangerousPropOps($objName, $stmt->else->stmts)) return true;
}
if ($stmt instanceof Node\Stmt\While_ || $stmt instanceof Node\Stmt\Do_) {
if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true;
}
if ($stmt instanceof Node\Stmt\For_ || $stmt instanceof Node\Stmt\Foreach_) {
if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true;
}
if ($stmt instanceof Node\Stmt\TryCatch) {
if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true;
foreach ($stmt->catches as $catch) {
if ($this->hasDangerousPropOps($objName, $catch->stmts)) return true;
}
if ($stmt->finally && $this->hasDangerousPropOps($objName, $stmt->finally->stmts)) return true;
}
if ($stmt instanceof Node\Stmt\Switch_) {
foreach ($stmt->cases as $case) {
if ($this->hasDangerousPropOps($objName, $case->stmts)) return true;
}
}
return false;
}
/**
* Check if an object variable is SSA-stable.
* Public for use by parsePropertyFetch() in code generation.
*/
public function isStableObject(string $objName): bool
{
return isset($this->context->stableObjects[$objName]);
}
/**
* Generate the property reference declaration for a stable object.
* Emits via beforeStmtLines so the reference is declared before the
* current statement at function scope.
*
* Skips hoisting when inside a loop or nested block scope, since the
* reference must be declared at function scope to be accessible later.
*/
public function hoistStableObjectProp(string $objName, string $propName, string $id, string $cType): string
{
$propVar = $this->getObjectPropVarName($objName, $propName);
if (isset($this->context->hoistedProps[$objName][$propName])) {
return $propVar;
}
if ($this->context->inLoop || $this->context->scopeLevel > 1) {
return $objName . '.attr(' . $id . ', true)';
}
$refGetter = $objName . '.attr(' . $id . ', true)';
$zvalMacro = ($cType === 'php::Float') ? 'Z_DVAL_P' : 'Z_LVAL_P';
$this->context->beforeStmtLines[] = $cType . ' &' . $propVar . ' = ' . $zvalMacro . '(' . $refGetter . '.unwrap_ptr());';
$this->context->hoistedProps[$objName][$propName] = true;
return $propVar;
}
}

@ -0,0 +1,20 @@
--TEST--
SSA: int
--FILE--
<?php
class Foo {
public int $a;
}
function main(): void {
$o = new Foo();
$o->a = 12;
$n = 1024;
while($n--) {
$o->a += 13;
}
var_dump($o->a);
}
?>
--EXPECT--
int(13324)

@ -0,0 +1,21 @@
--TEST--
SSA object prop: hoist float property to reference (NativeTypes)
--FILE--
<?php
use native_types;
class Foo {
public float $a;
}
function main(): void {
$o = new Foo();
$o->a = 1.5;
$n = 1024;
while($n--) {
$o->a += 0.5;
}
var_dump($o->a);
}
?>
--EXPECT--
float(513.5)

@ -0,0 +1,21 @@
--TEST--
SSA object prop: hoist int property to reference (NativeTypes)
--FILE--
<?php
use native_types;
class Foo {
public int $a;
}
function main(): void {
$o = new Foo();
$o->a = 12;
$n = 1024;
while($n--) {
$o->a += 13;
}
var_dump($o->a);
}
?>
--EXPECT--
int(13324)

@ -0,0 +1,23 @@
--TEST--
SSA object prop: __get/__set magic methods prevent hoisting
--FILE--
<?php
use native_types;
class Foo {
public int $a;
public function __get($name) { return $this->$name; }
public function __set($name, $value) { $this->$name = $value; }
}
function main(): void {
$o = new Foo();
$o->a = 10;
$n = 5;
while($n--) {
$o->a += 2;
}
var_dump($o->a);
}
?>
--EXPECT--
int(20)

@ -0,0 +1,23 @@
--TEST--
SSA object prop: multiple definitions prevent hoisting
--FILE--
<?php
use native_types;
class Foo {
public int $a;
}
function main(): void {
$o = new Foo();
$o->a = 100;
$o = new Foo();
$o->a = 200;
$n = 10;
while($n--) {
$o->a += 5;
}
var_dump($o->a);
}
?>
--EXPECT--
int(250)

@ -0,0 +1,24 @@
--TEST--
SSA object prop: reference capture of property prevents hoisting
--FILE--
<?php
use native_types;
class Foo {
public int $a;
}
function main(): void {
$o = new Foo();
$o->a = 10;
$ref = &$o->a;
$ref = 20;
$n = 5;
while($n--) {
$o->a += 1;
}
var_dump($o->a);
}
?>
--EXPECT--
int(25)
Loading…
Cancel
Save