- Implement exact-class fast path for zero-argument late-static calls with fallback mechanism - Add static property fetch resolution using typephp_get_static_property symbol - Enhance isset operations on static array properties with direct typephp_array_isset calls - Update class reference detection to return Type::STR for class constant fetch expressions - Add benchmark suite for static cache performance measurement including late static dispatch - Modify compiler symbols to use typephp variants for called class and static property access - Create comprehensive tests for static array single offset presence and late static call exact guards - Update unit tests to reflect new symbol names and variablemaster
parent
5689170839
commit
b3c7ee9bd3
16 changed files with 414 additions and 12 deletions
@ -0,0 +1,3 @@ |
||||
/build/ |
||||
/static_cache |
||||
/static_cache.exe |
||||
@ -0,0 +1,16 @@ |
||||
# Static class cache benchmark |
||||
|
||||
This benchmark covers a common metadata-cache pattern: a static array keyed by |
||||
`static::class`, guarded by `isset()`, plus a wrapper method using |
||||
`static::method()`. It measures static-property lookup, array lookup, strict |
||||
return checks, and late-static dispatch together. |
||||
|
||||
Run it from the repository root against matching Release PHP and PHPX builds: |
||||
|
||||
```bash |
||||
PHPX_HOME=../phpx PHP_BIN=/opt/php-8.5-nts/bin/php php benchmark/static-cache/run.php |
||||
``` |
||||
|
||||
The TypePHP binary is built with `-O3` and LTO. Results use the best of five |
||||
measured rounds after two warm-up rounds; checksums must match before ratios are |
||||
reported. Use `--skip-build` to reuse the existing binary. |
||||
@ -0,0 +1,75 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
const STATIC_CACHE_ITERATIONS = 1_000_000; |
||||
const STATIC_CACHE_WARMUPS = 2; |
||||
const STATIC_CACHE_ROUNDS = 5; |
||||
|
||||
class StaticCacheData |
||||
{ |
||||
public static array $cache = []; |
||||
|
||||
public static function getData(): array |
||||
{ |
||||
$class = static::class; |
||||
if (!isset(self::$cache[$class])) { |
||||
self::$cache[$class] = ['table' => 'users']; |
||||
} |
||||
return self::$cache[$class]; |
||||
} |
||||
|
||||
public static function getTable(): string |
||||
{ |
||||
return static::getData()['table']; |
||||
} |
||||
} |
||||
|
||||
function measureStaticCacheGetData(): array |
||||
{ |
||||
$best = PHP_FLOAT_MAX; |
||||
$checksum = 0; |
||||
for ($round = 0; $round < STATIC_CACHE_WARMUPS + STATIC_CACHE_ROUNDS; $round++) { |
||||
$start = hrtime(true); |
||||
for ($i = 0; $i < STATIC_CACHE_ITERATIONS; $i++) { |
||||
StaticCacheData::getData(); |
||||
} |
||||
$elapsed = hrtime(true) - $start; |
||||
if ($round >= STATIC_CACHE_WARMUPS && $elapsed < $best) { |
||||
$best = $elapsed; |
||||
} |
||||
$checksum += count(StaticCacheData::getData()); |
||||
} |
||||
return [$best / STATIC_CACHE_ITERATIONS, $checksum]; |
||||
} |
||||
|
||||
function measureStaticCacheGetTable(): array |
||||
{ |
||||
$best = PHP_FLOAT_MAX; |
||||
$checksum = 0; |
||||
for ($round = 0; $round < STATIC_CACHE_WARMUPS + STATIC_CACHE_ROUNDS; $round++) { |
||||
$start = hrtime(true); |
||||
for ($i = 0; $i < STATIC_CACHE_ITERATIONS; $i++) { |
||||
StaticCacheData::getTable(); |
||||
} |
||||
$elapsed = hrtime(true) - $start; |
||||
if ($round >= STATIC_CACHE_WARMUPS && $elapsed < $best) { |
||||
$best = $elapsed; |
||||
} |
||||
$checksum += strlen(StaticCacheData::getTable()); |
||||
} |
||||
return [$best / STATIC_CACHE_ITERATIONS, $checksum]; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
StaticCacheData::getData(); |
||||
|
||||
[$getData, $getDataChecksum] = measureStaticCacheGetData(); |
||||
[$getTable, $getTableChecksum] = measureStaticCacheGetTable(); |
||||
|
||||
echo "get_data_ns={$getData}\n"; |
||||
echo "get_table_ns={$getTable}\n"; |
||||
echo "checksum_get_data={$getDataChecksum}\n"; |
||||
echo "checksum_get_table={$getTableChecksum}\n"; |
||||
} |
||||
@ -0,0 +1,8 @@ |
||||
name: static_cache_benchmark |
||||
mode: bin |
||||
optimize: 3 |
||||
lto: true |
||||
build-dir: build |
||||
output: static_cache |
||||
sources: |
||||
- benchmark.php |
||||
@ -0,0 +1,124 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
$root = dirname(__DIR__, 2); |
||||
$source = __DIR__ . '/benchmark.php'; |
||||
$project = __DIR__ . '/project.yml'; |
||||
$binary = __DIR__ . '/static_cache' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : ''); |
||||
$skipBuild = in_array('--skip-build', $argv, true); |
||||
|
||||
/** @param list<string> $command */ |
||||
function runStaticCacheCommand(array $command, string $cwd, bool $capture, ?array $environment = null): string |
||||
{ |
||||
$process = proc_open( |
||||
$command, |
||||
[STDIN, $capture ? ['pipe', 'w'] : STDOUT, $capture ? ['pipe', 'w'] : STDERR], |
||||
$pipes, |
||||
$cwd, |
||||
$environment, |
||||
['bypass_shell' => true], |
||||
); |
||||
if (!is_resource($process)) { |
||||
throw new RuntimeException('Failed to start: ' . implode(' ', $command)); |
||||
} |
||||
|
||||
$output = $capture ? stream_get_contents($pipes[1]) : ''; |
||||
$error = $capture ? stream_get_contents($pipes[2]) : ''; |
||||
if ($capture) { |
||||
fclose($pipes[1]); |
||||
fclose($pipes[2]); |
||||
} |
||||
$status = proc_close($process); |
||||
if ($status !== 0) { |
||||
throw new RuntimeException(implode(' ', $command) . " failed ({$status})\n{$output}{$error}"); |
||||
} |
||||
return $output; |
||||
} |
||||
|
||||
/** @return array<string, float|string> */ |
||||
function parseStaticCacheResult(string $output): array |
||||
{ |
||||
$result = []; |
||||
foreach (explode("\n", trim($output)) as $line) { |
||||
if (preg_match('/^([a-z_]+)=([0-9.]+)$/', $line, $matches)) { |
||||
$result[$matches[1]] = str_starts_with($matches[1], 'checksum_') |
||||
? $matches[2] |
||||
: (float) $matches[2]; |
||||
} |
||||
} |
||||
return $result; |
||||
} |
||||
|
||||
$php = getenv('PHP_BIN') ?: PHP_BINARY; |
||||
$compilerPhp = getenv('TPC_PHP_BIN') ?: $php; |
||||
$probe = static fn (string $executable): string => trim(runStaticCacheCommand([ |
||||
$executable, |
||||
'-n', |
||||
'-r', |
||||
'printf("%s;%d;%d;%d", PHP_VERSION, PHP_ZTS, PHP_DEBUG, PHP_INT_SIZE);', |
||||
], $root, true)); |
||||
$phpRuntime = $probe($php); |
||||
$compilerRuntime = $probe($compilerPhp); |
||||
if ($phpRuntime !== $compilerRuntime) { |
||||
throw new RuntimeException("PHP ABI mismatch: PHP_BIN={$phpRuntime}; TPC_PHP_BIN={$compilerRuntime}"); |
||||
} |
||||
|
||||
if (!$skipBuild) { |
||||
runStaticCacheCommand([ |
||||
$compilerPhp, |
||||
'-n', |
||||
$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); |
||||
} |
||||
|
||||
$phpResult = parseStaticCacheResult(runStaticCacheCommand([ |
||||
$php, |
||||
'-n', |
||||
'-d', |
||||
'opcache.enable_cli=0', |
||||
'-d', |
||||
'opcache.jit=0', |
||||
'-r', |
||||
'require ' . var_export($source, true) . '; main();', |
||||
], $root, true)); |
||||
|
||||
$environment = getenv(); |
||||
if (PHP_OS_FAMILY !== 'Windows') { |
||||
$phpxHome = getenv('PHPX_HOME') ?: dirname($root) . '/phpx'; |
||||
$phpHome = dirname(dirname(realpath($compilerPhp) ?: $compilerPhp)); |
||||
$loader = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH'; |
||||
$existing = $environment[$loader] ?? ''; |
||||
$environment[$loader] = $phpxHome . '/lib' . PATH_SEPARATOR . $phpHome . '/lib' |
||||
. ($existing === '' ? '' : PATH_SEPARATOR . $existing); |
||||
} |
||||
$typephpResult = parseStaticCacheResult(runStaticCacheCommand([$binary], $root, true, $environment)); |
||||
|
||||
echo "Runtime: {$phpRuntime}\n"; |
||||
echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\n"; |
||||
echo "--------------------------------------------------\n"; |
||||
foreach (['get_data', 'get_table'] as $case) { |
||||
$metric = $case . '_ns'; |
||||
$checksum = 'checksum_' . $case; |
||||
if (!isset($phpResult[$metric], $typephpResult[$metric])) { |
||||
throw new RuntimeException("Missing benchmark metric: {$case}"); |
||||
} |
||||
if (($phpResult[$checksum] ?? null) !== ($typephpResult[$checksum] ?? null)) { |
||||
throw new RuntimeException("Checksum mismatch: {$case}"); |
||||
} |
||||
printf( |
||||
"%-12s %10.2f %14.2f %12.2fx\n", |
||||
$case, |
||||
$phpResult[$metric], |
||||
$typephpResult[$metric], |
||||
$typephpResult[$metric] / $phpResult[$metric], |
||||
); |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
--TEST-- |
||||
late static calls use the exact-class fast path without bypassing subclass dispatch |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class StaticCallBase |
||||
{ |
||||
public static function data(): array |
||||
{ |
||||
return [static::class, 'base']; |
||||
} |
||||
|
||||
public static function label(): string |
||||
{ |
||||
$data = static::data(); |
||||
return $data[0] . ':' . $data[1]; |
||||
} |
||||
} |
||||
|
||||
class StaticCallInherited extends StaticCallBase |
||||
{ |
||||
} |
||||
|
||||
class StaticCallOverride extends StaticCallBase |
||||
{ |
||||
public static function data(): array |
||||
{ |
||||
return [static::class, 'override']; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(StaticCallBase::label()); |
||||
var_dump(StaticCallInherited::label()); |
||||
var_dump(StaticCallOverride::label()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(19) "StaticCallBase:base" |
||||
string(24) "StaticCallInherited:base" |
||||
string(27) "StaticCallOverride:override" |
||||
@ -0,0 +1,46 @@ |
||||
--TEST-- |
||||
single-offset isset and coalesce on static arrays preserve PHP key and null semantics |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class StaticPresence |
||||
{ |
||||
public static array $values = [ |
||||
'present' => 42, |
||||
'null' => null, |
||||
2 => 'two', |
||||
'' => 'empty-key', |
||||
]; |
||||
|
||||
public static function has(mixed $key): bool |
||||
{ |
||||
return isset(self::$values[$key]); |
||||
} |
||||
|
||||
public static function get(mixed $key): mixed |
||||
{ |
||||
return self::$values[$key] ?? 'fallback'; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(StaticPresence::has('present')); |
||||
var_dump(StaticPresence::has('null')); |
||||
var_dump(StaticPresence::has('missing')); |
||||
var_dump(StaticPresence::has(2.9)); |
||||
var_dump(StaticPresence::has(null)); |
||||
var_dump(StaticPresence::get('present')); |
||||
var_dump(StaticPresence::get('null')); |
||||
var_dump(StaticPresence::get(null)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
bool(false) |
||||
bool(false) |
||||
bool(true) |
||||
bool(true) |
||||
int(42) |
||||
string(8) "fallback" |
||||
string(9) "empty-key" |
||||
Loading…
Reference in new issue