parent
91f5daf457
commit
e44007846b
13 changed files with 370 additions and 5 deletions
@ -0,0 +1,17 @@ |
||||
# TypePHP benchmarks |
||||
|
||||
This directory contains repeatable performance workloads used to guide and |
||||
verify compiler/runtime optimizations. Benchmark results depend on the CPU, |
||||
PHP build, compiler, and system load, so compare PHP and TypePHP on the same |
||||
machine instead of committing absolute timing expectations. |
||||
|
||||
- `bench.php` and `micro_bench.php` are the original general workloads moved |
||||
from `examples/`. |
||||
- `property-access/` builds and compares dynamic/static property access under |
||||
Zend PHP and TypePHP. |
||||
|
||||
Run the property benchmark from the repository root: |
||||
|
||||
```bash |
||||
php benchmark/property-access/run.php |
||||
``` |
||||
@ -0,0 +1,3 @@ |
||||
/build/ |
||||
/property_access |
||||
/*.rsp |
||||
@ -0,0 +1,15 @@ |
||||
# Dynamic property benchmark |
||||
|
||||
This benchmark compares the same dynamic and static property operations under |
||||
Zend PHP and a TypePHP `-O2` binary. Each metric is the best of seven rounds |
||||
after three warm-up rounds and is reported in nanoseconds per property access. |
||||
|
||||
Run it from the repository root: |
||||
|
||||
```bash |
||||
php benchmark/property-access/run.php |
||||
``` |
||||
|
||||
To reuse an existing binary, add `--skip-build`. For local regression checks, |
||||
`--max-ratio=1.5` exits unsuccessfully when a dynamic read or write takes more |
||||
than 1.5 times the corresponding Zend PHP result. |
||||
@ -0,0 +1,154 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
final class DynamicPropertyEntity |
||||
{ |
||||
public int $first = 0; |
||||
public int $second = 0; |
||||
public int $third = 0; |
||||
public int $fourth = 0; |
||||
public int $fifth = 0; |
||||
|
||||
public function hydrate(array $data): void |
||||
{ |
||||
foreach ($data as $property => $value) { |
||||
$this->$property = $value; |
||||
} |
||||
} |
||||
|
||||
public function sum(array $properties): int |
||||
{ |
||||
$sum = 0; |
||||
foreach ($properties as $property) { |
||||
$sum += $this->$property; |
||||
} |
||||
return $sum; |
||||
} |
||||
} |
||||
|
||||
final class StaticPropertyEntity |
||||
{ |
||||
public int $first = 0; |
||||
public int $second = 0; |
||||
public int $third = 0; |
||||
public int $fourth = 0; |
||||
public int $fifth = 0; |
||||
|
||||
public function hydrate(array $data): void |
||||
{ |
||||
$this->first = $data['first']; |
||||
$this->second = $data['second']; |
||||
$this->third = $data['third']; |
||||
$this->fourth = $data['fourth']; |
||||
$this->fifth = $data['fifth']; |
||||
} |
||||
|
||||
public function sum(): int |
||||
{ |
||||
return $this->first + $this->second + $this->third + $this->fourth + $this->fifth; |
||||
} |
||||
} |
||||
|
||||
function runDynamicWrite(DynamicPropertyEntity $entity, array $data, int $iterations): int |
||||
{ |
||||
for ($i = 0; $i < $iterations; $i++) { |
||||
$entity->hydrate($data); |
||||
} |
||||
return $entity->first; |
||||
} |
||||
|
||||
function runStaticWrite(StaticPropertyEntity $entity, array $data, int $iterations): int |
||||
{ |
||||
for ($i = 0; $i < $iterations; $i++) { |
||||
$entity->hydrate($data); |
||||
} |
||||
return $entity->first; |
||||
} |
||||
|
||||
function runDynamicRead(DynamicPropertyEntity $entity, array $properties, int $iterations): int |
||||
{ |
||||
$sum = 0; |
||||
for ($i = 0; $i < $iterations; $i++) { |
||||
$sum += $entity->sum($properties); |
||||
} |
||||
return $sum; |
||||
} |
||||
|
||||
function runStaticRead(StaticPropertyEntity $entity, int $iterations): int |
||||
{ |
||||
$sum = 0; |
||||
for ($i = 0; $i < $iterations; $i++) { |
||||
$sum += $entity->sum(); |
||||
} |
||||
return $sum; |
||||
} |
||||
|
||||
function measure(callable $callback, int $operations): float |
||||
{ |
||||
global $benchmarkSink; |
||||
for ($warmup = 0; $warmup < 3; $warmup++) { |
||||
$benchmarkSink += $callback(); |
||||
} |
||||
|
||||
$best = 1.0e30; |
||||
for ($round = 0; $round < 7; $round++) { |
||||
$start = hrtime(true); |
||||
$result = $callback(); |
||||
$elapsed = hrtime(true) - $start; |
||||
$benchmarkSink += $result; |
||||
if ($elapsed < $best) { |
||||
$best = $elapsed; |
||||
} |
||||
} |
||||
return $best / $operations; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
global $benchmarkSink; |
||||
$benchmarkSink = 0; |
||||
$iterations = 200000; |
||||
$data = [ |
||||
'first' => 1, |
||||
'second' => 2, |
||||
'third' => 3, |
||||
'fourth' => 4, |
||||
'fifth' => 5, |
||||
]; |
||||
$properties = ['first', 'second', 'third', 'fourth', 'fifth']; |
||||
$dynamic = new DynamicPropertyEntity(); |
||||
$static = new StaticPropertyEntity(); |
||||
$operations = $iterations * 5; |
||||
|
||||
$dynamicWrite = measure( |
||||
function () use ($dynamic, $data, $iterations): int { |
||||
return runDynamicWrite($dynamic, $data, $iterations); |
||||
}, |
||||
$operations, |
||||
); |
||||
$staticWrite = measure( |
||||
function () use ($static, $data, $iterations): int { |
||||
return runStaticWrite($static, $data, $iterations); |
||||
}, |
||||
$operations, |
||||
); |
||||
$dynamicRead = measure( |
||||
function () use ($dynamic, $properties, $iterations): int { |
||||
return runDynamicRead($dynamic, $properties, $iterations); |
||||
}, |
||||
$operations, |
||||
); |
||||
$staticRead = measure( |
||||
function () use ($static, $iterations): int { |
||||
return runStaticRead($static, $iterations); |
||||
}, |
||||
$operations, |
||||
); |
||||
|
||||
printf("dynamic_write_ns=%.3f\n", $dynamicWrite); |
||||
printf("static_write_ns=%.3f\n", $staticWrite); |
||||
printf("dynamic_read_ns=%.3f\n", $dynamicRead); |
||||
printf("static_read_ns=%.3f\n", $staticRead); |
||||
echo 'checksum=', $benchmarkSink + $dynamic->sum($properties) + $static->sum(), "\n"; |
||||
} |
||||
@ -0,0 +1,7 @@ |
||||
name: property_access_benchmark |
||||
mode: bin |
||||
optimize: 2 |
||||
build-dir: build |
||||
output: property_access |
||||
sources: |
||||
- benchmark.php |
||||
@ -0,0 +1,101 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
$root = dirname(__DIR__, 2); |
||||
$source = __DIR__ . '/benchmark.php'; |
||||
$project = __DIR__ . '/project.yml'; |
||||
$binary = __DIR__ . '/property_access'; |
||||
$skipBuild = in_array('--skip-build', $argv, true); |
||||
$maximumRatio = null; |
||||
foreach ($argv as $argument) { |
||||
if (str_starts_with($argument, '--max-ratio=')) { |
||||
$maximumRatio = (float) substr($argument, strlen('--max-ratio=')); |
||||
} |
||||
} |
||||
|
||||
/** @param list<string> $command */ |
||||
function runCommand(array $command, string $cwd, bool $capture): string |
||||
{ |
||||
$stdout = $capture ? ['pipe', 'w'] : STDOUT; |
||||
$stderr = $capture ? ['pipe', 'w'] : STDERR; |
||||
$process = proc_open($command, [STDIN, $stdout, $stderr], $pipes, $cwd, null, ['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<string, float> */ |
||||
function parseResults(string $output): array |
||||
{ |
||||
$results = []; |
||||
foreach (explode("\n", trim($output)) as $line) { |
||||
if (!str_contains($line, '=')) { |
||||
continue; |
||||
} |
||||
[$name, $value] = explode('=', $line, 2); |
||||
if ($name !== 'checksum') { |
||||
$results[$name] = (float) $value; |
||||
} |
||||
} |
||||
return $results; |
||||
} |
||||
|
||||
if (!$skipBuild) { |
||||
runCommand([ |
||||
PHP_BINARY, |
||||
$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); |
||||
} |
||||
|
||||
$php = parseResults(runCommand([ |
||||
PHP_BINARY, |
||||
'-d', |
||||
'opcache.enable_cli=0', |
||||
'-r', |
||||
'require ' . var_export($source, true) . '; main();', |
||||
], $root, true)); |
||||
$typephp = parseResults(runCommand([$binary], $root, true)); |
||||
|
||||
echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\n"; |
||||
echo "------------------------------------------------------------\n"; |
||||
$failed = false; |
||||
foreach (['dynamic_write_ns', 'dynamic_read_ns', 'static_write_ns', 'static_read_ns'] as $metric) { |
||||
if (!isset($php[$metric], $typephp[$metric])) { |
||||
throw new RuntimeException('Missing benchmark metric: ' . $metric); |
||||
} |
||||
$ratio = $typephp[$metric] / $php[$metric]; |
||||
printf("%-22s %10.2f %14.2f %12.2fx\n", $metric, $php[$metric], $typephp[$metric], $ratio); |
||||
if ($maximumRatio !== null && str_starts_with($metric, 'dynamic_') && $ratio > $maximumRatio) { |
||||
$failed = true; |
||||
} |
||||
} |
||||
|
||||
if ($failed) { |
||||
fwrite(STDERR, "Dynamic property ratio exceeded --max-ratio={$maximumRatio}\n"); |
||||
exit(1); |
||||
} |
||||
Loading…
Reference in new issue