Skip to content

Commit aca0f17

Browse files
committed
Benchmark stringification approach
1 parent 12c6dac commit aca0f17

1 file changed

Lines changed: 314 additions & 0 deletions

File tree

tests/benchmark_raw.php

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
<?php
2+
3+
/**
4+
* Micro-benchmark for Runtime::raw() implementation variants.
5+
*
6+
* Usage: php -d opcache.enable_cli=1 -d opcache.jit=tracing tests/benchmark_raw.php [iterations]
7+
*/
8+
$iterations = (int) ($argv[1] ?? 500_000);
9+
10+
// --- Implementations ---
11+
12+
function raw_current(mixed $value): string
13+
{
14+
if ($value === true) {
15+
return 'true';
16+
}
17+
18+
if ($value === false) {
19+
return 'false';
20+
}
21+
22+
if (is_array($value)) {
23+
if (array_is_list($value)) {
24+
$ret = '';
25+
foreach ($value as $vv) {
26+
$ret .= raw_current($vv) . ',';
27+
}
28+
return substr($ret, 0, -1);
29+
} else {
30+
return '[object Object]';
31+
}
32+
}
33+
34+
return "$value";
35+
}
36+
37+
// Add is_string() check before bool checks
38+
function raw_string_first(mixed $value): string
39+
{
40+
if (is_string($value)) {
41+
return $value;
42+
}
43+
44+
if ($value === true) {
45+
return 'true';
46+
}
47+
48+
if ($value === false) {
49+
return 'false';
50+
}
51+
52+
if (is_array($value)) {
53+
if (array_is_list($value)) {
54+
$ret = '';
55+
foreach ($value as $vv) {
56+
$ret .= raw_string_first($vv) . ',';
57+
}
58+
return substr($ret, 0, -1);
59+
} else {
60+
return '[object Object]';
61+
}
62+
}
63+
64+
return "$value";
65+
}
66+
67+
// Add null === check before the string cast
68+
function raw_null_early(mixed $value): string
69+
{
70+
if ($value === true) {
71+
return 'true';
72+
}
73+
74+
if ($value === false) {
75+
return 'false';
76+
}
77+
78+
if ($value === null) {
79+
return '';
80+
}
81+
82+
if (is_array($value)) {
83+
if (array_is_list($value)) {
84+
$ret = '';
85+
foreach ($value as $vv) {
86+
$ret .= raw_null_early($vv) . ',';
87+
}
88+
return substr($ret, 0, -1);
89+
} else {
90+
return '[object Object]';
91+
}
92+
}
93+
94+
return "$value";
95+
}
96+
97+
// is_string + null early returns before anything else
98+
function raw_string_null_first(mixed $value): string
99+
{
100+
if (is_string($value)) {
101+
return $value;
102+
}
103+
104+
if ($value === null) {
105+
return '';
106+
}
107+
108+
if ($value === true) {
109+
return 'true';
110+
}
111+
112+
if ($value === false) {
113+
return 'false';
114+
}
115+
116+
if (is_array($value)) {
117+
if (array_is_list($value)) {
118+
$ret = '';
119+
foreach ($value as $vv) {
120+
$ret .= raw_string_null_first($vv) . ',';
121+
}
122+
return substr($ret, 0, -1);
123+
} else {
124+
return '[object Object]';
125+
}
126+
}
127+
128+
return (string) $value;
129+
}
130+
131+
// Collapse two bool checks into one is_bool() call
132+
function raw_is_bool(mixed $value): string
133+
{
134+
if (is_bool($value)) {
135+
return $value ? 'true' : 'false';
136+
}
137+
138+
if (is_array($value)) {
139+
if (array_is_list($value)) {
140+
$ret = '';
141+
foreach ($value as $vv) {
142+
$ret .= raw_is_bool($vv) . ',';
143+
}
144+
return substr($ret, 0, -1);
145+
} else {
146+
return '[object Object]';
147+
}
148+
}
149+
150+
return "$value";
151+
}
152+
153+
// is_string first + is_bool combined
154+
function raw_string_is_bool(mixed $value): string
155+
{
156+
if (is_string($value)) {
157+
return $value;
158+
}
159+
160+
if (is_bool($value)) {
161+
return $value ? 'true' : 'false';
162+
}
163+
164+
if (is_array($value)) {
165+
if (array_is_list($value)) {
166+
$ret = '';
167+
foreach ($value as $vv) {
168+
$ret .= raw_string_is_bool($vv) . ',';
169+
}
170+
return substr($ret, 0, -1);
171+
} else {
172+
return '[object Object]';
173+
}
174+
}
175+
176+
return "$value";
177+
}
178+
179+
// --- Test cases ---
180+
181+
$cases = [
182+
'string' => 'Hello, world!',
183+
'null' => null,
184+
'int' => 42,
185+
'true' => true,
186+
'false' => false,
187+
'list arr' => ['a', 'b', 'c'],
188+
'assoc arr' => ['key' => 'val'],
189+
];
190+
191+
// Realistic mix: ~60% string, ~20% null, ~10% int, ~5% true, ~5% false
192+
$mix = [
193+
...array_fill(0, 60, 'Hello, world!'),
194+
...array_fill(0, 20, null),
195+
...array_fill(0, 10, 42),
196+
...array_fill(0, 5, true),
197+
...array_fill(0, 5, false),
198+
];
199+
shuffle($mix);
200+
$mixLen = count($mix);
201+
202+
$impls = [
203+
'current' => 'raw_current',
204+
'string_first' => 'raw_string_first',
205+
'null_early' => 'raw_null_early',
206+
'str+null_first' => 'raw_string_null_first',
207+
'is_bool' => 'raw_is_bool',
208+
'str+is_bool' => 'raw_string_is_bool',
209+
];
210+
211+
// --- Warmup: give JIT a chance to compile all paths ---
212+
213+
for ($w = 0; $w < 5_000; $w++) {
214+
foreach ($impls as $fn) {
215+
foreach ($cases as $val) {
216+
$fn($val);
217+
}
218+
}
219+
}
220+
221+
// --- Benchmark ---
222+
223+
$colLabels = array_merge(array_keys($cases), ['mix']);
224+
$nsWidth = 10; // "12.34 ns" = 8 chars + padding; header "assoc arr" = 9 chars
225+
$pctWidth = 10; // "+100%" = 5 chars + padding; header "assoc arr" = 9 chars
226+
$implWidth = 16;
227+
228+
$GREEN = "\033[32m";
229+
$RED = "\033[31m";
230+
$RESET = "\033[0m";
231+
$BOLD = "\033[1m";
232+
$RULE = "\033[2m";
233+
234+
$rule = fn(int $w) => $RULE . str_repeat('', $w) . $RESET . "\n";
235+
236+
function printCell(string $visible, int $width, string $color = '', string $reset = ''): void
237+
{
238+
$padded = str_pad($visible, $width, ' ', STR_PAD_LEFT);
239+
echo $color . $padded . $reset;
240+
}
241+
242+
// Collect all measurements first so we can print two clean tables.
243+
$ns = [];
244+
245+
foreach ($impls as $implLabel => $fn) {
246+
foreach ($colLabels as $caseLabel) {
247+
if ($caseLabel === 'mix') {
248+
$start = hrtime(true);
249+
for ($i = 0; $i < $iterations; $i++) {
250+
$fn($mix[$i % $mixLen]);
251+
}
252+
$ns[$implLabel][$caseLabel] = (hrtime(true) - $start) / $iterations;
253+
} else {
254+
$val = $cases[$caseLabel];
255+
$start = hrtime(true);
256+
for ($i = 0; $i < $iterations; $i++) {
257+
$fn($val);
258+
}
259+
$ns[$implLabel][$caseLabel] = (hrtime(true) - $start) / $iterations;
260+
}
261+
}
262+
}
263+
264+
$totalWidth = $implWidth + $nsWidth * count($colLabels);
265+
266+
// --- Table 1: ns / call ---
267+
268+
echo "\n{$BOLD}ns / call{$RESET} ({$iterations} iterations)\n";
269+
echo $rule($totalWidth);
270+
271+
printf("%-{$implWidth}s", '');
272+
foreach ($colLabels as $label) {
273+
printCell($label, $nsWidth);
274+
}
275+
echo "\n";
276+
echo $rule($totalWidth);
277+
278+
foreach ($impls as $implLabel => $_) {
279+
printf("%-{$implWidth}s", $implLabel);
280+
foreach ($colLabels as $caseLabel) {
281+
printCell(sprintf('%.2f ns', $ns[$implLabel][$caseLabel]), $nsWidth);
282+
}
283+
echo "\n";
284+
}
285+
286+
// --- Table 2: delta vs current ---
287+
288+
$totalWidth = $implWidth + $pctWidth * count($colLabels);
289+
290+
echo "\n{$BOLD}delta vs current{$RESET} (negative = faster)\n";
291+
echo $rule($totalWidth);
292+
293+
printf("%-{$implWidth}s", '');
294+
foreach ($colLabels as $label) {
295+
printCell($label, $pctWidth);
296+
}
297+
echo "\n";
298+
echo $rule($totalWidth);
299+
300+
foreach ($impls as $implLabel => $_) {
301+
if ($implLabel === 'current') {
302+
continue;
303+
}
304+
printf("%-{$implWidth}s", $implLabel);
305+
foreach ($colLabels as $caseLabel) {
306+
$pct = ($ns[$implLabel][$caseLabel] - $ns['current'][$caseLabel]) / $ns['current'][$caseLabel] * 100;
307+
$visible = sprintf('%+.0f%%', $pct);
308+
$color = $pct < -1.0 ? $GREEN : ($pct > 1.0 ? $RED : '');
309+
printCell($visible, $pctWidth, $color, $color !== '' ? $RESET : '');
310+
}
311+
echo "\n";
312+
}
313+
314+
echo "\nRun with: php -d opcache.enable_cli=1 -d opcache.jit=tracing tests/benchmark_raw.php\n";

0 commit comments

Comments
 (0)