Skip to content

Commit e440078

Browse files
committed
perf: use native property handlers for ordinary classes
1 parent 91f5daf commit e440078

13 files changed

Lines changed: 370 additions & 5 deletions

File tree

README-CN.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -524,9 +524,9 @@ TypePHP 使用 `-O3` 运行 PHP 源码树自带的官方 `bench.php` 与
524524
| `micro_bench.php`(总计) | 13.045 秒 | **2.021 秒** | 约 6.5× |
525525

526526
两项基准覆盖 PHP 语言核心性能——函数调用、对象属性访问、数组/哈希访问、
527-
字符串处理、控制流等。仓库内的测试源码为
528-
[`examples/bench.php`](examples/bench.php)
529-
[`examples/micro_bench.php`](examples/micro_bench.php)
527+
字符串处理、控制流等。测试代码见 [`benchmark/bench.php`](benchmark/bench.php) 和
528+
[`benchmark/micro_bench.php`](benchmark/micro_bench.php)。其他专项性能回归测试
529+
统一放置在 [`benchmark/`](benchmark/) 目录中
530530

531531
这些数字是项目测量快照,不是性能保证。PHP 版本、编译器、CPU、优化参数和已启用
532532
扩展都会影响结果;在用于部署决策前,应在同一机器上使用相同 workload 自行对比。

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -565,8 +565,9 @@ benchmarks that ship with the PHP source tree, compiled with `-O3`:
565565

566566
Both benchmarks measure core PHP language performance — function calls, object
567567
property access, array/hash access, string handling, control flow, and more.
568-
The checked-in workloads are [`examples/bench.php`](examples/bench.php) and
569-
[`examples/micro_bench.php`](examples/micro_bench.php).
568+
The checked-in workloads are [`benchmark/bench.php`](benchmark/bench.php) and
569+
[`benchmark/micro_bench.php`](benchmark/micro_bench.php). Additional focused
570+
performance regressions live in the same [`benchmark/`](benchmark/) directory.
570571

571572
These numbers are a project measurement snapshot, not a performance guarantee.
572573
PHP version, compiler, CPU, optimization flags, and enabled extensions can all

benchmark/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# TypePHP benchmarks
2+
3+
This directory contains repeatable performance workloads used to guide and
4+
verify compiler/runtime optimizations. Benchmark results depend on the CPU,
5+
PHP build, compiler, and system load, so compare PHP and TypePHP on the same
6+
machine instead of committing absolute timing expectations.
7+
8+
- `bench.php` and `micro_bench.php` are the original general workloads moved
9+
from `examples/`.
10+
- `property-access/` builds and compares dynamic/static property access under
11+
Zend PHP and TypePHP.
12+
13+
Run the property benchmark from the repository root:
14+
15+
```bash
16+
php benchmark/property-access/run.php
17+
```
File renamed without changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/build/
2+
/property_access
3+
/*.rsp
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Dynamic property benchmark
2+
3+
This benchmark compares the same dynamic and static property operations under
4+
Zend PHP and a TypePHP `-O2` binary. Each metric is the best of seven rounds
5+
after three warm-up rounds and is reported in nanoseconds per property access.
6+
7+
Run it from the repository root:
8+
9+
```bash
10+
php benchmark/property-access/run.php
11+
```
12+
13+
To reuse an existing binary, add `--skip-build`. For local regression checks,
14+
`--max-ratio=1.5` exits unsuccessfully when a dynamic read or write takes more
15+
than 1.5 times the corresponding Zend PHP result.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
final class DynamicPropertyEntity
6+
{
7+
public int $first = 0;
8+
public int $second = 0;
9+
public int $third = 0;
10+
public int $fourth = 0;
11+
public int $fifth = 0;
12+
13+
public function hydrate(array $data): void
14+
{
15+
foreach ($data as $property => $value) {
16+
$this->$property = $value;
17+
}
18+
}
19+
20+
public function sum(array $properties): int
21+
{
22+
$sum = 0;
23+
foreach ($properties as $property) {
24+
$sum += $this->$property;
25+
}
26+
return $sum;
27+
}
28+
}
29+
30+
final class StaticPropertyEntity
31+
{
32+
public int $first = 0;
33+
public int $second = 0;
34+
public int $third = 0;
35+
public int $fourth = 0;
36+
public int $fifth = 0;
37+
38+
public function hydrate(array $data): void
39+
{
40+
$this->first = $data['first'];
41+
$this->second = $data['second'];
42+
$this->third = $data['third'];
43+
$this->fourth = $data['fourth'];
44+
$this->fifth = $data['fifth'];
45+
}
46+
47+
public function sum(): int
48+
{
49+
return $this->first + $this->second + $this->third + $this->fourth + $this->fifth;
50+
}
51+
}
52+
53+
function runDynamicWrite(DynamicPropertyEntity $entity, array $data, int $iterations): int
54+
{
55+
for ($i = 0; $i < $iterations; $i++) {
56+
$entity->hydrate($data);
57+
}
58+
return $entity->first;
59+
}
60+
61+
function runStaticWrite(StaticPropertyEntity $entity, array $data, int $iterations): int
62+
{
63+
for ($i = 0; $i < $iterations; $i++) {
64+
$entity->hydrate($data);
65+
}
66+
return $entity->first;
67+
}
68+
69+
function runDynamicRead(DynamicPropertyEntity $entity, array $properties, int $iterations): int
70+
{
71+
$sum = 0;
72+
for ($i = 0; $i < $iterations; $i++) {
73+
$sum += $entity->sum($properties);
74+
}
75+
return $sum;
76+
}
77+
78+
function runStaticRead(StaticPropertyEntity $entity, int $iterations): int
79+
{
80+
$sum = 0;
81+
for ($i = 0; $i < $iterations; $i++) {
82+
$sum += $entity->sum();
83+
}
84+
return $sum;
85+
}
86+
87+
function measure(callable $callback, int $operations): float
88+
{
89+
global $benchmarkSink;
90+
for ($warmup = 0; $warmup < 3; $warmup++) {
91+
$benchmarkSink += $callback();
92+
}
93+
94+
$best = 1.0e30;
95+
for ($round = 0; $round < 7; $round++) {
96+
$start = hrtime(true);
97+
$result = $callback();
98+
$elapsed = hrtime(true) - $start;
99+
$benchmarkSink += $result;
100+
if ($elapsed < $best) {
101+
$best = $elapsed;
102+
}
103+
}
104+
return $best / $operations;
105+
}
106+
107+
function main(): void
108+
{
109+
global $benchmarkSink;
110+
$benchmarkSink = 0;
111+
$iterations = 200000;
112+
$data = [
113+
'first' => 1,
114+
'second' => 2,
115+
'third' => 3,
116+
'fourth' => 4,
117+
'fifth' => 5,
118+
];
119+
$properties = ['first', 'second', 'third', 'fourth', 'fifth'];
120+
$dynamic = new DynamicPropertyEntity();
121+
$static = new StaticPropertyEntity();
122+
$operations = $iterations * 5;
123+
124+
$dynamicWrite = measure(
125+
function () use ($dynamic, $data, $iterations): int {
126+
return runDynamicWrite($dynamic, $data, $iterations);
127+
},
128+
$operations,
129+
);
130+
$staticWrite = measure(
131+
function () use ($static, $data, $iterations): int {
132+
return runStaticWrite($static, $data, $iterations);
133+
},
134+
$operations,
135+
);
136+
$dynamicRead = measure(
137+
function () use ($dynamic, $properties, $iterations): int {
138+
return runDynamicRead($dynamic, $properties, $iterations);
139+
},
140+
$operations,
141+
);
142+
$staticRead = measure(
143+
function () use ($static, $iterations): int {
144+
return runStaticRead($static, $iterations);
145+
},
146+
$operations,
147+
);
148+
149+
printf("dynamic_write_ns=%.3f\n", $dynamicWrite);
150+
printf("static_write_ns=%.3f\n", $staticWrite);
151+
printf("dynamic_read_ns=%.3f\n", $dynamicRead);
152+
printf("static_read_ns=%.3f\n", $staticRead);
153+
echo 'checksum=', $benchmarkSink + $dynamic->sum($properties) + $static->sum(), "\n";
154+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name: property_access_benchmark
2+
mode: bin
3+
optimize: 2
4+
build-dir: build
5+
output: property_access
6+
sources:
7+
- benchmark.php

benchmark/property-access/run.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
$root = dirname(__DIR__, 2);
6+
$source = __DIR__ . '/benchmark.php';
7+
$project = __DIR__ . '/project.yml';
8+
$binary = __DIR__ . '/property_access';
9+
$skipBuild = in_array('--skip-build', $argv, true);
10+
$maximumRatio = null;
11+
foreach ($argv as $argument) {
12+
if (str_starts_with($argument, '--max-ratio=')) {
13+
$maximumRatio = (float) substr($argument, strlen('--max-ratio='));
14+
}
15+
}
16+
17+
/** @param list<string> $command */
18+
function runCommand(array $command, string $cwd, bool $capture): string
19+
{
20+
$stdout = $capture ? ['pipe', 'w'] : STDOUT;
21+
$stderr = $capture ? ['pipe', 'w'] : STDERR;
22+
$process = proc_open($command, [STDIN, $stdout, $stderr], $pipes, $cwd, null, ['bypass_shell' => true]);
23+
if (!is_resource($process)) {
24+
throw new RuntimeException('Failed to start: ' . implode(' ', $command));
25+
}
26+
27+
$output = '';
28+
$error = '';
29+
if ($capture) {
30+
$output = stream_get_contents($pipes[1]);
31+
$error = stream_get_contents($pipes[2]);
32+
fclose($pipes[1]);
33+
fclose($pipes[2]);
34+
}
35+
$status = proc_close($process);
36+
if ($status !== 0) {
37+
throw new RuntimeException(
38+
'Command failed (' . $status . '): ' . implode(' ', $command) . "\n" . $output . $error,
39+
);
40+
}
41+
return $output;
42+
}
43+
44+
/** @return array<string, float> */
45+
function parseResults(string $output): array
46+
{
47+
$results = [];
48+
foreach (explode("\n", trim($output)) as $line) {
49+
if (!str_contains($line, '=')) {
50+
continue;
51+
}
52+
[$name, $value] = explode('=', $line, 2);
53+
if ($name !== 'checksum') {
54+
$results[$name] = (float) $value;
55+
}
56+
}
57+
return $results;
58+
}
59+
60+
if (!$skipBuild) {
61+
runCommand([
62+
PHP_BINARY,
63+
$root . '/bin/tpc.php',
64+
$project,
65+
'-j',
66+
'8',
67+
'--no-color',
68+
'--no-progress',
69+
], $root, false);
70+
}
71+
if (!is_file($binary)) {
72+
throw new RuntimeException('Benchmark binary does not exist: ' . $binary);
73+
}
74+
75+
$php = parseResults(runCommand([
76+
PHP_BINARY,
77+
'-d',
78+
'opcache.enable_cli=0',
79+
'-r',
80+
'require ' . var_export($source, true) . '; main();',
81+
], $root, true));
82+
$typephp = parseResults(runCommand([$binary], $root, true));
83+
84+
echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\n";
85+
echo "------------------------------------------------------------\n";
86+
$failed = false;
87+
foreach (['dynamic_write_ns', 'dynamic_read_ns', 'static_write_ns', 'static_read_ns'] as $metric) {
88+
if (!isset($php[$metric], $typephp[$metric])) {
89+
throw new RuntimeException('Missing benchmark metric: ' . $metric);
90+
}
91+
$ratio = $typephp[$metric] / $php[$metric];
92+
printf("%-22s %10.2f %14.2f %12.2fx\n", $metric, $php[$metric], $typephp[$metric], $ratio);
93+
if ($maximumRatio !== null && str_starts_with($metric, 'dynamic_') && $ratio > $maximumRatio) {
94+
$failed = true;
95+
}
96+
}
97+
98+
if ($failed) {
99+
fwrite(STDERR, "Dynamic property ratio exceeded --max-ratio={$maximumRatio}\n");
100+
exit(1);
101+
}

0 commit comments

Comments
 (0)