perf: use native property handlers for ordinary classes

master
韩天峰 2 hours ago
parent 91f5daf457
commit e44007846b
  1. 6
      README-CN.md
  2. 5
      README.md
  3. 17
      benchmark/README.md
  4. 0
      benchmark/bench.php
  5. 0
      benchmark/micro_bench.php
  6. 3
      benchmark/property-access/.gitignore
  7. 15
      benchmark/property-access/README.md
  8. 154
      benchmark/property-access/benchmark.php
  9. 7
      benchmark/property-access/project.yml
  10. 101
      benchmark/property-access/run.php
  11. 7
      phpunit/code/new-object-codegen.php
  12. 34
      phpunit/src/NewObjectCodegenTest.php
  13. 26
      src/Translator.php

@ -524,9 +524,9 @@ TypePHP 使用 `-O3` 运行 PHP 源码树自带的官方 `bench.php` 与
| `micro_bench.php`(总计) | 13.045 秒 | **2.021 秒** | 约 6.5× |
两项基准覆盖 PHP 语言核心性能——函数调用、对象属性访问、数组/哈希访问、
字符串处理、控制流等。仓库内的测试源码为
[`examples/bench.php`](examples/bench.php)和
[`examples/micro_bench.php`](examples/micro_bench.php)
字符串处理、控制流等。测试代码见 [`benchmark/bench.php`](benchmark/bench.php) 和
[`benchmark/micro_bench.php`](benchmark/micro_bench.php)。其他专项性能回归测试
统一放置在 [`benchmark/`](benchmark/) 目录中
这些数字是项目测量快照,不是性能保证。PHP 版本、编译器、CPU、优化参数和已启用
扩展都会影响结果;在用于部署决策前,应在同一机器上使用相同 workload 自行对比。

@ -565,8 +565,9 @@ benchmarks that ship with the PHP source tree, compiled with `-O3`:
Both benchmarks measure core PHP language performance — function calls, object
property access, array/hash access, string handling, control flow, and more.
The checked-in workloads are [`examples/bench.php`](examples/bench.php) and
[`examples/micro_bench.php`](examples/micro_bench.php).
The checked-in workloads are [`benchmark/bench.php`](benchmark/bench.php) and
[`benchmark/micro_bench.php`](benchmark/micro_bench.php). Additional focused
performance regressions live in the same [`benchmark/`](benchmark/) directory.
These numbers are a project measurement snapshot, not a performance guarantee.
PHP version, compiler, CPU, optimization flags, and enabled extensions can all

@ -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);
}

@ -48,6 +48,13 @@ class HookOnlyDefaultCodegen
}
}
class GetterOnlyDefaultCodegen
{
public string $value {
get => 'computed';
}
}
class AsymmetricOnlyDefaultCodegen
{
public private(set) int $value = 0;

@ -119,6 +119,40 @@ final class NewObjectCodegenTest extends \BaseTest
);
}
public function testOrdinaryClassesKeepNativeReadAndWritePropertyHandlers(): void
{
[, $extension] = $this->compileFixtureAndExtension();
self::assertStringContainsString(
'property_handlers_KnownNewObjectCodegen.read_property = '
. 'base_property_handlers_KnownNewObjectCodegen->read_property;',
$extension,
);
self::assertStringContainsString(
'property_handlers_KnownNewObjectCodegen.write_property = '
. 'base_property_handlers_KnownNewObjectCodegen->write_property;',
$extension,
);
}
public function testHookAndAsymmetricClassesKeepOnlyRequiredDispatchHandlers(): void
{
[, $extension] = $this->compileFixtureAndExtension();
self::assertStringNotContainsString('base_property_handlers_HookOnlyDefaultCodegen', $extension);
self::assertStringNotContainsString('base_property_handlers_GetterOnlyDefaultCodegen', $extension);
self::assertStringContainsString(
'property_handlers_AsymmetricOnlyDefaultCodegen.read_property = '
. 'base_property_handlers_AsymmetricOnlyDefaultCodegen->read_property;',
$extension,
);
self::assertStringNotContainsString(
'property_handlers_AsymmetricOnlyDefaultCodegen.write_property = '
. 'base_property_handlers_AsymmetricOnlyDefaultCodegen->write_property;',
$extension,
);
}
private function compileFixture(): string
{
return $this->compileFixtureAndExtension()[0];

@ -2274,7 +2274,33 @@ CODE;
return $body . "return obj;\n";
};
$needsHookReadHandler = false;
$needsHookWriteHandler = false;
foreach ($classDef->properties as $property) {
if ($property->getter !== null) {
$needsHookReadHandler = true;
$needsHookWriteHandler = true;
}
if ($property->setter !== null || $property->isPrivateSet() || $property->isProtectedSet()) {
$needsHookWriteHandler = true;
}
}
if (!$needsHookReadHandler || !$needsHookWriteHandler) {
$baseHandlers = "base_property_handlers_{$className}";
// Keep the inherited handlers before PHPX installs the
// TypePHP unset/clone table. Most classes have no hooks;
// routing every ordinary property access through the hook
// name lookup makes dynamic properties several times
// slower even though the lookup can never succeed.
$code .= "const auto *{$baseHandlers} = {$ce}->default_object_handlers;\n";
}
$code .= "typephp_install_property_handlers({$ce}, &{$handlers});\n";
if (!$needsHookReadHandler) {
$code .= "{$handlers}.read_property = {$baseHandlers}->read_property;\n";
}
if (!$needsHookWriteHandler) {
$code .= "{$handlers}.write_property = {$baseHandlers}->write_property;\n";
}
if ($classDef->requireCtor) {
$code .= "create_object_{$className} = php::getCreateObjectFn({$ce});\n";
$code .= "{$ce}->create_object = [](zend_class_entry *class_type) -> zend_object* {\n";

Loading…
Cancel
Save