- 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