diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index c9d138fe..eaea4727 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -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; diff --git a/src/Php/Context/FunctionContext.php b/src/Php/Context/FunctionContext.php index 3dd64ce0..c40f8e25 100644 --- a/src/Php/Context/FunctionContext.php +++ b/src/Php/Context/FunctionContext.php @@ -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 */ @@ -64,6 +70,8 @@ class FunctionContext $this->stdArrays = []; $this->stdContainers = []; $this->objectProps = []; + $this->stableObjects = []; + $this->hoistedProps = []; $this->ceWrappers = []; $this->tmpVarIndex = 0; $this->scopeLayouts = []; diff --git a/src/Php/Optimizer/SsaPropOptimizer.php b/src/Php/Optimizer/SsaPropOptimizer.php new file mode 100644 index 00000000..4aaa4f19 --- /dev/null +++ b/src/Php/Optimizer/SsaPropOptimizer.php @@ -0,0 +1,401 @@ +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; + } +} diff --git a/tests/aot/optimizations/object-int-prop.phpt b/tests/aot/optimizations/object-int-prop.phpt new file mode 100644 index 00000000..536b5199 --- /dev/null +++ b/tests/aot/optimizations/object-int-prop.phpt @@ -0,0 +1,20 @@ +--TEST-- +SSA: int +--FILE-- +a = 12; + + $n = 1024; + while($n--) { + $o->a += 13; + } + var_dump($o->a); +} +?> +--EXPECT-- +int(13324) \ No newline at end of file diff --git a/tests/aot/optimizations/objprop-hoist-float.phpt b/tests/aot/optimizations/objprop-hoist-float.phpt new file mode 100644 index 00000000..a17c8337 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-float.phpt @@ -0,0 +1,21 @@ +--TEST-- +SSA object prop: hoist float property to reference (NativeTypes) +--FILE-- +a = 1.5; + + $n = 1024; + while($n--) { + $o->a += 0.5; + } + var_dump($o->a); +} +?> +--EXPECT-- +float(513.5) diff --git a/tests/aot/optimizations/objprop-hoist-int.phpt b/tests/aot/optimizations/objprop-hoist-int.phpt new file mode 100644 index 00000000..7aa77260 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-int.phpt @@ -0,0 +1,21 @@ +--TEST-- +SSA object prop: hoist int property to reference (NativeTypes) +--FILE-- +a = 12; + + $n = 1024; + while($n--) { + $o->a += 13; + } + var_dump($o->a); +} +?> +--EXPECT-- +int(13324) diff --git a/tests/aot/optimizations/objprop-hoist-magic.phpt b/tests/aot/optimizations/objprop-hoist-magic.phpt new file mode 100644 index 00000000..90e07d78 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-magic.phpt @@ -0,0 +1,23 @@ +--TEST-- +SSA object prop: __get/__set magic methods prevent hoisting +--FILE-- +$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) diff --git a/tests/aot/optimizations/objprop-hoist-multi-def.phpt b/tests/aot/optimizations/objprop-hoist-multi-def.phpt new file mode 100644 index 00000000..86196421 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-multi-def.phpt @@ -0,0 +1,23 @@ +--TEST-- +SSA object prop: multiple definitions prevent hoisting +--FILE-- +a = 100; + $o = new Foo(); + $o->a = 200; + + $n = 10; + while($n--) { + $o->a += 5; + } + var_dump($o->a); +} +?> +--EXPECT-- +int(250) diff --git a/tests/aot/optimizations/objprop-hoist-ref-capture.phpt b/tests/aot/optimizations/objprop-hoist-ref-capture.phpt new file mode 100644 index 00000000..d3429b90 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-ref-capture.phpt @@ -0,0 +1,24 @@ +--TEST-- +SSA object prop: reference capture of property prevents hoisting +--FILE-- +a = 10; + + $ref = &$o->a; + $ref = 20; + + $n = 5; + while($n--) { + $o->a += 1; + } + var_dump($o->a); +} +?> +--EXPECT-- +int(25)