From 681c7d42e7d0b0553d01a91f9cbd19a8f18d6ac0 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sat, 5 Sep 2026 18:38:29 +0800 Subject: [PATCH] perf(compiler): skip caches for variable method names --- phpunit/code/call-cache-sites.php | 1 + phpunit/src/CallCacheCodegenTest.php | 11 +++--- phpunit/src/SymbolTest.php | 15 ------- src/Generator/Symbol.php | 15 ------- src/Parser/MethodCallTrait.php | 19 +++++++++ src/Parser/NullsafeAccessTrait.php | 21 +++++++--- .../dynamic_call/call-cache-dispatch.phpt | 39 ++++++++++++------- 7 files changed, 65 insertions(+), 56 deletions(-) diff --git a/phpunit/code/call-cache-sites.php b/phpunit/code/call-cache-sites.php index c09e8f98..8a3f0f64 100644 --- a/phpunit/code/call-cache-sites.php +++ b/phpunit/code/call-cache-sites.php @@ -12,6 +12,7 @@ function call_cache_sites(mixed $callback, object $object, ?object $nullable, mi $object->$method(2), $object->fixedMethod(3), $nullable?->nullableMethod(4), + $nullable?->$method(5), ]; } diff --git a/phpunit/src/CallCacheCodegenTest.php b/phpunit/src/CallCacheCodegenTest.php index fe2ce967..ee5f953b 100644 --- a/phpunit/src/CallCacheCodegenTest.php +++ b/phpunit/src/CallCacheCodegenTest.php @@ -28,16 +28,17 @@ final class CallCacheCodegenTest extends BaseTest self::assertSame(1, substr_count($code, 'typephp_call_cached(')); self::assertSame(3, substr_count($code, 'php::callStaticMethod(')); self::assertStringNotContainsString('php::concat({', $code); - self::assertSame(7, substr_count($code, 'php::VarList{')); + self::assertSame(8, substr_count($code, 'php::VarList{')); self::assertStringNotContainsString('std::arrayassertEquals('php::getStaticProperty', Symbol::getStaticProperty()); } - public function testGetResolvedStaticProperty(): void - { - $this->assertEquals('typephp_get_static_property', Symbol::getResolvedStaticProperty()); - } - public function testSetStaticProperty(): void { $this->assertEquals('php::setStaticProperty', Symbol::setStaticProperty()); @@ -52,16 +47,6 @@ class SymbolTest extends TestCase $this->assertEquals('php::VarList', Symbol::varList()); } - public function testGetCalledCe(): void - { - $this->assertSame('typephp_get_called_ce(this_)', Symbol::getCalledCe()); - } - - public function testGetCalledClass(): void - { - $this->assertSame('typephp_get_called_class(this_)', Symbol::getCalledClass()); - } - public function testSafeIndex(): void { $result = Symbol::safeIndex('0', '10'); diff --git a/src/Generator/Symbol.php b/src/Generator/Symbol.php index 7129ae21..90cb02b7 100644 --- a/src/Generator/Symbol.php +++ b/src/Generator/Symbol.php @@ -15,11 +15,6 @@ class Symbol return 'php::getStaticProperty'; } - public static function getResolvedStaticProperty(): string - { - return 'typephp_get_static_property'; - } - public static function getStaticPropertyRef(): string { return 'php::getStaticPropertyRef'; @@ -40,16 +35,6 @@ class Symbol return 'php::concat'; } - public static function getCalledCe(): string - { - return 'typephp_get_called_ce(this_)'; - } - - public static function getCalledClass(): string - { - return 'typephp_get_called_class(this_)'; - } - public static function constant(): string { return 'php::constant'; diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index ebcbe414..31aadef2 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -840,6 +840,11 @@ trait MethodCallTrait } else { $funcName = ''; } + // A variable method name explicitly requests runtime dispatch. Such a + // call site commonly receives unrelated route/callback names, so a + // monomorphic method cache adds guards and request state without a + // reliable hit rate. Cache only a source-level identifier. + $cacheMethod = $this->isNamedMethod($expr->name); $requiresDynamicScope = $this->runtimeMethodRequiresDynamicScope( $class, @@ -862,6 +867,10 @@ trait MethodCallTrait if (empty($expr->args)) { if ($requiresDynamicScope && $this->methodDef) { if (!$resolvedMethodPtr) { + if (!$cacheMethod) { + return 'php::callScoped(' . $object . ', ' . $methodPtr . ', ' + . $this->getCallableScopeExpr() . ')'; + } return 'typephp_call_method_scoped_cached(' . $object . ', ' . $methodPtr . ', ' . $this->getCallableScopeExpr() . ', ' . $this->getMethodCallCache() . ')'; } @@ -871,6 +880,9 @@ trait MethodCallTrait return 'php::callScoped(' . $object . ', ' . $methodPtr . ', ' . $this->getCallableScopeExpr() . ')'; } if (!$resolvedMethodPtr) { + if (!$cacheMethod) { + return $object . '.call(' . $methodPtr . ')'; + } return 'typephp_call_method_cached(' . $object . ', ' . $methodPtr . ', ' . $this->getMethodCallCache() . ')'; } @@ -881,10 +893,17 @@ trait MethodCallTrait if (!$resolvedMethodPtr) { $callArgs = $this->parseCallArgs($expr->args, $funcName, $class); if ($requiresDynamicScope && $this->methodDef) { + if (!$cacheMethod) { + return 'php::callScoped(' . $object . ', ' . $methodPtr . ', ' + . $this->getCallableScopeExpr() . ', ' . $callArgs . ')'; + } return 'typephp_call_method_scoped_cached(' . $object . ', ' . $methodPtr . ', ' . $this->getCallableScopeExpr() . ', ' . $this->getMethodCallCache() . ', ' . $callArgs . ')'; } + if (!$cacheMethod) { + return $object . '.call(' . $methodPtr . ', ' . $callArgs . ')'; + } return 'typephp_call_method_cached(' . $object . ', ' . $methodPtr . ', ' . $this->getMethodCallCache() . ', ' . $callArgs . ')'; } diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index 24e4d810..b65f3f0f 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -118,13 +118,22 @@ trait NullsafeAccessTrait $code .= $this->formatCapturedStmtLines($argBeforeStmts); } if ($requiresDynamicScope && $this->methodDef) { - $code .= $this->getIndent() - . "{$tmpVar} = typephp_call_method_scoped_cached({$object}, {$item[1]}, " - . $this->getCallableScopeExpr() . ', ' . $this->getMethodCallCache() - . ", {$args});" . PHP_EOL; + if ($this->isNamedMethod($item[4]->name)) { + $code .= $this->getIndent() + . "{$tmpVar} = typephp_call_method_scoped_cached({$object}, {$item[1]}, " + . $this->getCallableScopeExpr() . ', ' . $this->getMethodCallCache() + . ", {$args});" . PHP_EOL; + } else { + $code .= $this->getIndent() . "{$tmpVar} = php::callScoped({$object}, {$item[1]}, " + . $this->getCallableScopeExpr() . ", {$args});" . PHP_EOL; + } } else { - $code .= $this->getIndent() . "{$tmpVar} = typephp_call_method_cached({$object}, {$item[1]}, " - . $this->getMethodCallCache() . ", {$args});" . PHP_EOL; + if ($this->isNamedMethod($item[4]->name)) { + $code .= $this->getIndent() . "{$tmpVar} = typephp_call_method_cached({$object}, {$item[1]}, " + . $this->getMethodCallCache() . ", {$args});" . PHP_EOL; + } else { + $code .= $this->getIndent() . "{$tmpVar} = {$object}.call({$item[1]}, {$args});" . PHP_EOL; + } } if ($argAfterStmts) { $code .= $this->formatCapturedStmtLines($argAfterStmts); diff --git a/tests/compiler/dynamic_call/call-cache-dispatch.phpt b/tests/compiler/dynamic_call/call-cache-dispatch.phpt index 59c67c6f..91e34e81 100644 --- a/tests/compiler/dynamic_call/call-cache-dispatch.phpt +++ b/tests/compiler/dynamic_call/call-cache-dispatch.phpt @@ -1,5 +1,5 @@ --TEST-- -Dynamic call caches preserve polymorphic, object, and magic dispatch +Dynamic calls preserve cached names, dynamic names, polymorphism, and magic dispatch --FILE-- run($value); } +function invoke_nullsafe_dynamic_method(?object $object, mixed $method, int $value): ?string +{ + return $object?->$method($value); +} + function invoke_static_method(mixed $class, mixed $method, int $value): string { return $class::$method($value); @@ -139,16 +144,18 @@ function main(): void var_dump(invoke_nullsafe_named_method($objects[0], 11)); var_dump(invoke_nullsafe_named_method(null, 12)); var_dump(invoke_nullsafe_named_method($objects[1], 13)); + var_dump(invoke_nullsafe_dynamic_method($objects[0], 'run', 14)); + var_dump(invoke_nullsafe_dynamic_method(null, 'run', 15)); $scoped = new CachedScopedMethod(); - var_dump($scoped->invoke('hidden', 14)); - var_dump($scoped->invoke('hidden', 15)); + var_dump($scoped->invoke('hidden', 16)); + var_dump($scoped->invoke('hidden', 17)); - var_dump(invoke_static_method('CachedStaticMethod', 'run', 16)); - var_dump(invoke_static_method('CachedStaticMethodSecond', 'run', 17)); - var_dump(invoke_static_method('CachedStaticMagic', 'missing', 18)); - var_dump(invoke_static_named_method('CachedStaticMethod', 19)); - var_dump(invoke_named_class_dynamic_method('run', 20)); + var_dump(invoke_static_method('CachedStaticMethod', 'run', 18)); + var_dump(invoke_static_method('CachedStaticMethodSecond', 'run', 19)); + var_dump(invoke_static_method('CachedStaticMagic', 'missing', 20)); + var_dump(invoke_static_named_method('CachedStaticMethod', 21)); + var_dump(invoke_named_class_dynamic_method('run', 22)); } ?> --EXPECT-- @@ -166,10 +173,12 @@ string(16) "magic-missing:10" string(15) "method-first:11" NULL string(16) "method-second:13" -string(9) "scoped:14" -string(9) "scoped:15" -string(9) "static:16" -string(16) "static-second:17" -string(23) "static-magic-missing:18" -string(9) "static:19" -string(9) "static:20" +string(15) "method-first:14" +NULL +string(9) "scoped:16" +string(9) "scoped:17" +string(9) "static:18" +string(16) "static-second:19" +string(23) "static-magic-missing:20" +string(9) "static:21" +string(9) "static:22"