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