parent
1c16639418
commit
683a7c6ec0
16 changed files with 715 additions and 4 deletions
@ -0,0 +1,3 @@ |
|||||||
|
/build/ |
||||||
|
/dynamic_call |
||||||
|
/*.rsp |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
# Dynamic call benchmark |
||||||
|
|
||||||
|
This benchmark compares direct calls with the runtime callable forms handled |
||||||
|
by PHPX. It deliberately separates stable call sites from alternating and |
||||||
|
megamorphic sites. TypePHP's function-call cache keeps one name inline and |
||||||
|
promotes polymorphic sites to a request-local table. Its method-call cache is |
||||||
|
monomorphic and disables itself after observing a different class or name. |
||||||
|
|
||||||
|
It also measures dynamic method names with a stable receiver, alternating |
||||||
|
method names, and a fixed method name on changing receiver classes. Those |
||||||
|
cases require a class-entry guard in addition to a callable-name guard. |
||||||
|
|
||||||
|
Run it from the repository root against a release PHP/PHPX build: |
||||||
|
|
||||||
|
```bash |
||||||
|
PHPX_HOME=../phpx PHP_BIN=/opt/php-8.5-nts/bin/php php benchmark/dynamic-call/run.php |
||||||
|
``` |
||||||
|
|
||||||
|
The TypePHP binary is built with `-O3` and LTO. `PHP_BIN` selects both the Zend |
||||||
|
PHP baseline and, by default, the PHP executable used to run the compiler. |
||||||
|
`TPC_PHP_BIN` may override the latter, but the runner rejects different PHP |
||||||
|
versions, ZTS/debug modes, or integer widths. PHPX must also be a Release build |
||||||
|
for that same PHP ABI. Add `--skip-build` to reuse the binary or |
||||||
|
`--case=<name>` to measure one workload while profiling. |
||||||
|
|
||||||
|
Reported values are the best of five rounds after two warm-up rounds. The |
||||||
|
checksum must match between PHP and TypePHP; absolute timing is intentionally |
||||||
|
not used as a pass/fail condition. |
||||||
@ -0,0 +1,284 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
const DYNAMIC_CALL_ITERATIONS = 1_000_000; |
||||||
|
const DYNAMIC_CALL_ROUNDS = 5; |
||||||
|
|
||||||
|
function dynamicCallAddOne(int $value): int |
||||||
|
{ |
||||||
|
return $value + 1; |
||||||
|
} |
||||||
|
|
||||||
|
function dynamicCallAddTwo(int $value): int |
||||||
|
{ |
||||||
|
return $value + 2; |
||||||
|
} |
||||||
|
|
||||||
|
function dynamicCallAddThree(int $value): int |
||||||
|
{ |
||||||
|
return $value + 3; |
||||||
|
} |
||||||
|
|
||||||
|
function dynamicCallAddFour(int $value): int |
||||||
|
{ |
||||||
|
return $value + 4; |
||||||
|
} |
||||||
|
|
||||||
|
function dynamicCallAddFive(int $value): int |
||||||
|
{ |
||||||
|
return $value + 5; |
||||||
|
} |
||||||
|
|
||||||
|
function dynamicCallAddSix(int $value): int |
||||||
|
{ |
||||||
|
return $value + 6; |
||||||
|
} |
||||||
|
|
||||||
|
function dynamicCallAddSeven(int $value): int |
||||||
|
{ |
||||||
|
return $value + 7; |
||||||
|
} |
||||||
|
|
||||||
|
function dynamicCallAddEight(int $value): int |
||||||
|
{ |
||||||
|
return $value + 8; |
||||||
|
} |
||||||
|
|
||||||
|
final class DynamicCallTarget |
||||||
|
{ |
||||||
|
public static function addOne(int $value): int |
||||||
|
{ |
||||||
|
return $value + 1; |
||||||
|
} |
||||||
|
|
||||||
|
public function addTwo(int $value): int |
||||||
|
{ |
||||||
|
return $value + 2; |
||||||
|
} |
||||||
|
|
||||||
|
public function hitOne(int $value): int |
||||||
|
{ |
||||||
|
return $value + 1; |
||||||
|
} |
||||||
|
|
||||||
|
public function hitTwo(int $value): int |
||||||
|
{ |
||||||
|
return $value + 2; |
||||||
|
} |
||||||
|
|
||||||
|
public function __invoke(int $value): int |
||||||
|
{ |
||||||
|
return $value + 3; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class DynamicCallAlternateTarget |
||||||
|
{ |
||||||
|
public function hitOne(int $value): int |
||||||
|
{ |
||||||
|
return $value + 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function runDirectCall(int $iterations): int |
||||||
|
{ |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$sum += dynamicCallAddOne($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runMonomorphicStringCall(int $iterations): int |
||||||
|
{ |
||||||
|
$callback = 'dynamicCallAddOne'; |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$sum += $callback($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runAlternatingStringCall(int $iterations): int |
||||||
|
{ |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$callback = ($i & 1) === 0 ? 'dynamicCallAddOne' : 'dynamicCallAddTwo'; |
||||||
|
$sum += $callback($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runMegamorphicStringCall(int $iterations): int |
||||||
|
{ |
||||||
|
$callbacks = [ |
||||||
|
'dynamicCallAddOne', |
||||||
|
'dynamicCallAddTwo', |
||||||
|
'dynamicCallAddThree', |
||||||
|
'dynamicCallAddFour', |
||||||
|
'dynamicCallAddFive', |
||||||
|
'dynamicCallAddSix', |
||||||
|
'dynamicCallAddSeven', |
||||||
|
'dynamicCallAddEight', |
||||||
|
]; |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$callback = $callbacks[($i * 5 + 3) & 7]; |
||||||
|
$sum += $callback($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runMonomorphicClosureCall(int $iterations): int |
||||||
|
{ |
||||||
|
$callback = static fn (int $value): int => $value + 1; |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$sum += $callback($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runAlternatingClosureCall(int $iterations): int |
||||||
|
{ |
||||||
|
$first = static fn (int $value): int => $value + 1; |
||||||
|
$second = static fn (int $value): int => $value + 2; |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$callback = ($i & 1) === 0 ? $first : $second; |
||||||
|
$sum += $callback($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runStaticMethodStringCall(int $iterations): int |
||||||
|
{ |
||||||
|
$callback = 'DynamicCallTarget::addOne'; |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$sum += $callback($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runObjectMethodArrayCall(int $iterations): int |
||||||
|
{ |
||||||
|
$target = new DynamicCallTarget(); |
||||||
|
$callback = [$target, 'addTwo']; |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$sum += $callback($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runInvokableObjectCall(int $iterations): int |
||||||
|
{ |
||||||
|
$callback = new DynamicCallTarget(); |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$sum += $callback($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runMonomorphicMethodNameCall(int $iterations): int |
||||||
|
{ |
||||||
|
$target = new DynamicCallTarget(); |
||||||
|
$method = 'hitOne'; |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$sum += $target->$method($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runAlternatingMethodNameCall(int $iterations): int |
||||||
|
{ |
||||||
|
$target = new DynamicCallTarget(); |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$method = ($i & 1) === 0 ? 'hitOne' : 'hitTwo'; |
||||||
|
$sum += $target->$method($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runPolymorphicMethodReceiverCall(int $iterations): int |
||||||
|
{ |
||||||
|
$targets = [new DynamicCallTarget(), new DynamicCallAlternateTarget()]; |
||||||
|
$method = 'hitOne'; |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $iterations; $i++) { |
||||||
|
$target = $targets[$i & 1]; |
||||||
|
$sum += $target->$method($i); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
|
||||||
|
function runDynamicCallCase(string $case, int $iterations): int |
||||||
|
{ |
||||||
|
return match ($case) { |
||||||
|
'direct' => runDirectCall($iterations), |
||||||
|
'string_monomorphic' => runMonomorphicStringCall($iterations), |
||||||
|
'string_alternating' => runAlternatingStringCall($iterations), |
||||||
|
'string_megamorphic' => runMegamorphicStringCall($iterations), |
||||||
|
'closure_monomorphic' => runMonomorphicClosureCall($iterations), |
||||||
|
'closure_alternating' => runAlternatingClosureCall($iterations), |
||||||
|
'static_method_string' => runStaticMethodStringCall($iterations), |
||||||
|
'object_method_array' => runObjectMethodArrayCall($iterations), |
||||||
|
'invokable_object' => runInvokableObjectCall($iterations), |
||||||
|
'method_name_monomorphic' => runMonomorphicMethodNameCall($iterations), |
||||||
|
'method_name_alternating' => runAlternatingMethodNameCall($iterations), |
||||||
|
'method_receiver_polymorphic' => runPolymorphicMethodReceiverCall($iterations), |
||||||
|
default => throw new RuntimeException("Unknown benchmark case: {$case}"), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function measureDynamicCallCase(string $case): array |
||||||
|
{ |
||||||
|
for ($warmup = 0; $warmup < 2; $warmup++) { |
||||||
|
runDynamicCallCase($case, 1_000); |
||||||
|
} |
||||||
|
|
||||||
|
$best = 1.0e30; |
||||||
|
$bestResult = 0; |
||||||
|
for ($round = 0; $round < DYNAMIC_CALL_ROUNDS; $round++) { |
||||||
|
$start = hrtime(true); |
||||||
|
$result = runDynamicCallCase($case, DYNAMIC_CALL_ITERATIONS); |
||||||
|
$elapsed = hrtime(true) - $start; |
||||||
|
if ($elapsed < $best) { |
||||||
|
$best = $elapsed; |
||||||
|
$bestResult = $result; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return [$best / DYNAMIC_CALL_ITERATIONS, $bestResult]; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$selectedCase = getenv('DYNAMIC_CALL_CASE'); |
||||||
|
foreach ([ |
||||||
|
'direct', |
||||||
|
'string_monomorphic', |
||||||
|
'string_alternating', |
||||||
|
'string_megamorphic', |
||||||
|
'closure_monomorphic', |
||||||
|
'closure_alternating', |
||||||
|
'static_method_string', |
||||||
|
'object_method_array', |
||||||
|
'invokable_object', |
||||||
|
'method_name_monomorphic', |
||||||
|
'method_name_alternating', |
||||||
|
'method_receiver_polymorphic', |
||||||
|
] as $case) { |
||||||
|
if (is_string($selectedCase) && $selectedCase !== '' && $selectedCase !== $case) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
[$nanoseconds, $result] = measureDynamicCallCase($case); |
||||||
|
printf("%s_ns=%.3f\n", $case, $nanoseconds); |
||||||
|
printf("checksum_%s=%d\n", $case, $result); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
name: dynamic_call_benchmark |
||||||
|
mode: bin |
||||||
|
optimize: 3 |
||||||
|
lto: true |
||||||
|
build-dir: build |
||||||
|
output: dynamic_call |
||||||
|
sources: |
||||||
|
- benchmark.php |
||||||
@ -0,0 +1,162 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
$root = dirname(__DIR__, 2); |
||||||
|
$source = __DIR__ . '/benchmark.php'; |
||||||
|
$project = __DIR__ . '/project.yml'; |
||||||
|
$binary = __DIR__ . '/dynamic_call' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : ''); |
||||||
|
$skipBuild = in_array('--skip-build', $argv, true); |
||||||
|
$selectedCase = null; |
||||||
|
foreach ($argv as $argument) { |
||||||
|
if (str_starts_with($argument, '--case=')) { |
||||||
|
$selectedCase = substr($argument, strlen('--case=')); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<string> $command |
||||||
|
* @param array<string, string>|null $environment |
||||||
|
*/ |
||||||
|
function runDynamicCallCommand(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, |
||||||
|
$environment, |
||||||
|
['bypass_shell' => true], |
||||||
|
); |
||||||
|
if (!is_resource($process)) { |
||||||
|
throw new RuntimeException('Failed to start: ' . implode(' ', $command)); |
||||||
|
} |
||||||
|
|
||||||
|
$output = ''; |
||||||
|
$error = ''; |
||||||
|
if ($capture) { |
||||||
|
$output = stream_get_contents($pipes[1]); |
||||||
|
$error = stream_get_contents($pipes[2]); |
||||||
|
fclose($pipes[1]); |
||||||
|
fclose($pipes[2]); |
||||||
|
} |
||||||
|
$status = proc_close($process); |
||||||
|
if ($status !== 0) { |
||||||
|
throw new RuntimeException( |
||||||
|
'Command failed (' . $status . '): ' . implode(' ', $command) . "\n" . $output . $error, |
||||||
|
); |
||||||
|
} |
||||||
|
return $output; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return array{metrics: array<string, float>, checksums: array<string, string>} */ |
||||||
|
function parseDynamicCallResults(string $output): array |
||||||
|
{ |
||||||
|
$metrics = []; |
||||||
|
$checksums = []; |
||||||
|
foreach (explode("\n", trim($output)) as $line) { |
||||||
|
if (preg_match('/^([a-z_]+)_ns=([0-9.]+)$/', $line, $matches)) { |
||||||
|
$metrics[$matches[1]] = (float) $matches[2]; |
||||||
|
} elseif (preg_match('/^checksum_([a-z_]+)=(-?[0-9]+)$/', $line, $matches)) { |
||||||
|
$checksums[$matches[1]] = $matches[2]; |
||||||
|
} |
||||||
|
} |
||||||
|
return ['metrics' => $metrics, 'checksums' => $checksums]; |
||||||
|
} |
||||||
|
|
||||||
|
$baselinePhp = getenv('PHP_BIN') ?: PHP_BINARY; |
||||||
|
$compilerPhp = getenv('TPC_PHP_BIN') ?: $baselinePhp; |
||||||
|
$runtimeProbe = static fn (string $binary): string => trim(runDynamicCallCommand([ |
||||||
|
$binary, |
||||||
|
'-n', |
||||||
|
'-r', |
||||||
|
'printf("%s;%d;%d;%d", PHP_VERSION, PHP_ZTS, PHP_DEBUG, PHP_INT_SIZE);', |
||||||
|
], $root, true)); |
||||||
|
$baselineRuntime = $runtimeProbe($baselinePhp); |
||||||
|
$compilerRuntime = $runtimeProbe($compilerPhp); |
||||||
|
if ($baselineRuntime !== $compilerRuntime) { |
||||||
|
throw new RuntimeException( |
||||||
|
"PHP_BIN and TPC_PHP_BIN must use the same PHP ABI for comparable results\n" |
||||||
|
. "PHP_BIN={$baselineRuntime}\nTPC_PHP_BIN={$compilerRuntime}", |
||||||
|
); |
||||||
|
} |
||||||
|
if (!$skipBuild) { |
||||||
|
echo "Building TypePHP benchmark (-O3 + LTO)...\n"; |
||||||
|
runDynamicCallCommand([ |
||||||
|
$compilerPhp, |
||||||
|
$root . '/bin/tpc.php', |
||||||
|
$project, |
||||||
|
'-j', |
||||||
|
'8', |
||||||
|
'--no-color', |
||||||
|
'--no-progress', |
||||||
|
], $root, false); |
||||||
|
} |
||||||
|
if (!is_file($binary)) { |
||||||
|
throw new RuntimeException('Benchmark binary does not exist: ' . $binary); |
||||||
|
} |
||||||
|
|
||||||
|
$environment = getenv(); |
||||||
|
if ($selectedCase !== null && $selectedCase !== '') { |
||||||
|
$environment['DYNAMIC_CALL_CASE'] = $selectedCase; |
||||||
|
} |
||||||
|
|
||||||
|
$php = parseDynamicCallResults(runDynamicCallCommand([ |
||||||
|
$baselinePhp, |
||||||
|
'-n', |
||||||
|
'-d', |
||||||
|
'opcache.enable_cli=0', |
||||||
|
'-d', |
||||||
|
'opcache.jit=0', |
||||||
|
'-r', |
||||||
|
'require ' . var_export($source, true) . '; main();', |
||||||
|
], $root, true, $environment)); |
||||||
|
|
||||||
|
if (PHP_OS_FAMILY !== 'Windows') { |
||||||
|
$phpxHome = getenv('PHPX_HOME') ?: dirname($root) . '/phpx'; |
||||||
|
$loaderVariable = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH'; |
||||||
|
$existing = $environment[$loaderVariable] ?? ''; |
||||||
|
$phpHome = dirname(dirname(realpath($compilerPhp) ?: $compilerPhp)); |
||||||
|
$environment[$loaderVariable] = $phpxHome . '/lib' . PATH_SEPARATOR . $phpHome . '/lib' |
||||||
|
. ($existing === '' ? '' : PATH_SEPARATOR . $existing); |
||||||
|
} |
||||||
|
$typephp = parseDynamicCallResults(runDynamicCallCommand([$binary], $root, true, $environment)); |
||||||
|
|
||||||
|
$cases = [ |
||||||
|
'direct', |
||||||
|
'string_monomorphic', |
||||||
|
'string_alternating', |
||||||
|
'string_megamorphic', |
||||||
|
'closure_monomorphic', |
||||||
|
'closure_alternating', |
||||||
|
'static_method_string', |
||||||
|
'object_method_array', |
||||||
|
'invokable_object', |
||||||
|
'method_name_monomorphic', |
||||||
|
'method_name_alternating', |
||||||
|
'method_receiver_polymorphic', |
||||||
|
]; |
||||||
|
if ($selectedCase !== null && $selectedCase !== '') { |
||||||
|
$cases = [$selectedCase]; |
||||||
|
} |
||||||
|
|
||||||
|
echo "Runtime: {$baselineRuntime}\n"; |
||||||
|
echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\n"; |
||||||
|
echo "--------------------------------------------------------------\n"; |
||||||
|
foreach ($cases as $case) { |
||||||
|
if (!isset($php['metrics'][$case], $typephp['metrics'][$case])) { |
||||||
|
throw new RuntimeException("Missing benchmark metric: {$case}"); |
||||||
|
} |
||||||
|
if (($php['checksums'][$case] ?? null) !== ($typephp['checksums'][$case] ?? null)) { |
||||||
|
throw new RuntimeException("Checksum mismatch for benchmark case: {$case}"); |
||||||
|
} |
||||||
|
printf( |
||||||
|
"%-25s %10.2f %14.2f %12.2fx\n", |
||||||
|
$case, |
||||||
|
$php['metrics'][$case], |
||||||
|
$typephp['metrics'][$case], |
||||||
|
$typephp['metrics'][$case] / $php['metrics'][$case], |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
function call_cache_target(int $value): int |
||||||
|
{ |
||||||
|
return $value + 1; |
||||||
|
} |
||||||
|
|
||||||
|
function call_cache_sites(mixed $callback, object $object, mixed $method): array |
||||||
|
{ |
||||||
|
return [$callback(1), $object->$method(2)]; |
||||||
|
} |
||||||
|
|
||||||
|
class CallCacheScopedTarget |
||||||
|
{ |
||||||
|
private function hidden(): int |
||||||
|
{ |
||||||
|
return 3; |
||||||
|
} |
||||||
|
|
||||||
|
public function invoke(mixed $method): int |
||||||
|
{ |
||||||
|
return $this->$method(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
} |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
use TypePhp\CompilerBase; |
||||||
|
use TypePhp\CompilerTest; |
||||||
|
|
||||||
|
final class CallCacheCodegenTest extends BaseTest |
||||||
|
{ |
||||||
|
public function testDynamicCallSitesUseRequestLocalTypePhpCaches(): void |
||||||
|
{ |
||||||
|
global $translator; |
||||||
|
|
||||||
|
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||||
|
$translator = $compiler; |
||||||
|
$compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); |
||||||
|
$compiler->setTargetName('call_cache_sites'); |
||||||
|
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/call-cache-sites.php'; |
||||||
|
$compiler->addFiles([$source]); |
||||||
|
$compiler->prepareFile($source); |
||||||
|
$generated = $compiler->convertFile($source); |
||||||
|
$code = file_get_contents($generated); |
||||||
|
$extension = file_get_contents($compiler->genExtension()); |
||||||
|
|
||||||
|
self::assertIsString($code); |
||||||
|
self::assertIsString($extension); |
||||||
|
self::assertSame(1, substr_count($code, 'typephp_call_cached(')); |
||||||
|
self::assertSame(1, substr_count($code, 'typephp_call_method_cached(')); |
||||||
|
self::assertStringContainsString('php::callScoped(', $code); |
||||||
|
|
||||||
|
self::assertStringContainsString('php::FunctionCallCacheSlot function_call_cache_map[1]', $extension); |
||||||
|
self::assertStringContainsString('php::MethodCallCacheSlot method_call_cache_map[1]', $extension); |
||||||
|
self::assertStringContainsString('typephp_get_function_call_cache(FunctionCallCacheId cache_id)', $extension); |
||||||
|
self::assertStringContainsString('typephp_get_method_call_cache(MethodCallCacheId cache_id)', $extension); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,85 @@ |
|||||||
|
--TEST-- |
||||||
|
Dynamic call caches preserve polymorphic, object, and magic dispatch |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function cached_first(int $value): string |
||||||
|
{ |
||||||
|
return 'first:' . $value; |
||||||
|
} |
||||||
|
|
||||||
|
function cached_second(int $value): string |
||||||
|
{ |
||||||
|
return 'second:' . $value; |
||||||
|
} |
||||||
|
|
||||||
|
class CachedMethodFirst |
||||||
|
{ |
||||||
|
public function run(int $value): string |
||||||
|
{ |
||||||
|
return 'method-first:' . $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class CachedMethodSecond |
||||||
|
{ |
||||||
|
public function run(int $value): string |
||||||
|
{ |
||||||
|
return 'method-second:' . $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class CachedMagicMethod |
||||||
|
{ |
||||||
|
public function __call(string $name, array $arguments): string |
||||||
|
{ |
||||||
|
return 'magic-' . $name . ':' . $arguments[0]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class CachedStaticMethod |
||||||
|
{ |
||||||
|
public static function run(int $value): string |
||||||
|
{ |
||||||
|
return 'static:' . $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function invoke_function(mixed $callback, int $value): string |
||||||
|
{ |
||||||
|
return $callback($value); |
||||||
|
} |
||||||
|
|
||||||
|
function invoke_method(object $object, mixed $method, int $value): string |
||||||
|
{ |
||||||
|
return $object->$method($value); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$callbacks = ['cached_first', 'cached_second', 'cached_first']; |
||||||
|
foreach ($callbacks as $index => $callback) { |
||||||
|
var_dump(invoke_function($callback, $index)); |
||||||
|
} |
||||||
|
|
||||||
|
$static = 'CachedStaticMethod::run'; |
||||||
|
var_dump(invoke_function($static, 3)); |
||||||
|
|
||||||
|
$closure = static fn (int $value): string => 'closure:' . $value; |
||||||
|
var_dump(invoke_function($closure, 4)); |
||||||
|
|
||||||
|
$objects = [new CachedMethodFirst(), new CachedMethodSecond(), new CachedMagicMethod()]; |
||||||
|
foreach ($objects as $index => $object) { |
||||||
|
var_dump(invoke_method($object, $index === 2 ? 'missing' : 'run', $index + 5)); |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(7) "first:0" |
||||||
|
string(8) "second:1" |
||||||
|
string(7) "first:2" |
||||||
|
string(8) "static:3" |
||||||
|
string(9) "closure:4" |
||||||
|
string(14) "method-first:5" |
||||||
|
string(15) "method-second:6" |
||||||
|
string(15) "magic-missing:7" |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: shutdown finalizers can use request-local dynamic call caches |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function shutdown_dynamic_target(string $value): string |
||||||
|
{ |
||||||
|
return 'finalizer:' . $value; |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class ShutdownDynamicCaller |
||||||
|
{ |
||||||
|
public function __destruct() |
||||||
|
{ |
||||||
|
$callback = 'shutdown_dynamic_target'; |
||||||
|
echo $callback('ok') . "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
global $shutdownObject; |
||||||
|
$shutdownObject = new ShutdownDynamicCaller(); |
||||||
|
echo "main\n"; |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
main |
||||||
|
finalizer:ok |
||||||
Loading…
Reference in new issue