perf(optimizer): lower simple integer min/max calls (#96)
* perf(optimizer): lower simple integer min and max calls * docs(benchmark): focus min/max results on integer performancemaster
parent
393ea1d369
commit
692841a60b
13 changed files with 385 additions and 0 deletions
@ -0,0 +1,35 @@ |
||||
# Integer min/max benchmark |
||||
|
||||
This benchmark repeatedly clamps integer values and accumulates a checksum. It |
||||
isolates two-argument integer `min()` / `max()` calls; it is not representative |
||||
of every PHP workload. It emits one line after the loop, so terminal output is |
||||
not part of the hot path. |
||||
|
||||
From this directory, with PHP 8.5 and a matching PHPX/embed installation: |
||||
|
||||
```sh |
||||
php run.php 10000000 |
||||
php ../../bin/tpc.php project.yml --no-progress -o min_max_benchmark |
||||
./min_max_benchmark 10000000 |
||||
``` |
||||
|
||||
The PHP and AOT checksums must match. Compile baseline and candidate revisions |
||||
into different build directories and binary paths, then alternate their |
||||
execution order. Exclude compilation time, discard a warm-up pair, and compare |
||||
medians over multiple runs. Keep PHPX, PHP, compiler flags, and machine fixed. |
||||
|
||||
## Sample result |
||||
|
||||
Linux ARM64 in Docker, PHP 8.5.10 ZTS, PHPX `6a68f38`, GCC `-O2`; |
||||
baseline TypePHP `72b7ce9b` versus this integer min/max lowering change. |
||||
Nine measured runs per binary after one discarded pair, 10,000,000 iterations: |
||||
|
||||
| Build | Median | Range | |
||||
| --- | ---: | ---: | |
||||
| Baseline | 482.56 ms | 456.41–497.78 ms | |
||||
| Integer lowering | 48.01 ms | 44.10–57.38 ms | |
||||
|
||||
This is a 10.05x speedup for the isolated integer min/max workload. Timings |
||||
come from a shared development machine, not a dedicated benchmark host. Raw |
||||
samples are in `results-arm64.json`; times include process startup and exclude |
||||
compilation. |
||||
@ -0,0 +1,15 @@ |
||||
<?php |
||||
|
||||
function main(int $argc, array $argv): void |
||||
{ |
||||
$iterations = $argc > 1 ? (int) $argv[1] : 10000000; |
||||
$checksum = 0; |
||||
for ($i = 0; $i < $iterations; ++$i) { |
||||
$damage = ($i % 101) - 20; |
||||
$hp = 100 - ($i % 100); |
||||
$damage = max(0, $damage); |
||||
$remaining = min($hp, $damage); |
||||
$checksum += $remaining; |
||||
} |
||||
echo $checksum, "\n"; |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
name: min-max-benchmark |
||||
build-mode: bin |
||||
optimize: 2 |
||||
sources: |
||||
- benchmark.php |
||||
@ -0,0 +1,35 @@ |
||||
{ |
||||
"baseline": "72b7ce9b", |
||||
"candidate": "simple integer min/max lowering", |
||||
"php": "8.5.10 ZTS", |
||||
"phpx": "6a68f38", |
||||
"platform": "Linux ARM64 Docker", |
||||
"optimization": "O2", |
||||
"method": "alternate order; one discarded pair; nine measured pairs; wall time including process startup; seconds", |
||||
"count": 10000000, |
||||
"checksum": "236310950", |
||||
"samples": { |
||||
"candidate": [ |
||||
0.057375312, |
||||
0.045864502, |
||||
0.048057849, |
||||
0.045604484, |
||||
0.048013895, |
||||
0.045256348, |
||||
0.04409891, |
||||
0.048323617, |
||||
0.049788737 |
||||
], |
||||
"baseline": [ |
||||
0.482051066, |
||||
0.482560355, |
||||
0.478532421, |
||||
0.497780501, |
||||
0.484743119, |
||||
0.456406973, |
||||
0.472020001, |
||||
0.483321787, |
||||
0.489281674 |
||||
] |
||||
} |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
<?php |
||||
|
||||
require __DIR__ . '/benchmark.php'; |
||||
|
||||
main($argc, $argv); |
||||
@ -0,0 +1,29 @@ |
||||
<?php |
||||
function minMaxIntegers(int $a, int $b): array |
||||
{ |
||||
return [min($a, $b), max($a, $b)]; |
||||
} |
||||
function minMaxFallbacks(mixed $a, mixed $b): array |
||||
{ |
||||
return [min($a, $b), max($a, $b), min(1.5, 2.5), max(1, 2.5), min([3, 1]), max(1, 2, 3), min(...[3, 1])]; |
||||
} |
||||
function minMaxUnproven(mixed $a): array |
||||
{ |
||||
return [min($a + 1, 2), max($a + 1, 2)]; |
||||
} |
||||
class MinMaxNullableProperty |
||||
{ |
||||
public ?int $value = null; |
||||
} |
||||
function minMaxNullable(?int $value, MinMaxNullableProperty $object): array |
||||
{ |
||||
return [min($value, 0), max($value, 0), min($object->value, 0), max($object->value, 0)]; |
||||
} |
||||
function minMaxCasts(mixed $a): array |
||||
{ |
||||
return [min((int) $a, 2), max((int) $a, 2)]; |
||||
} |
||||
function main(): void |
||||
{ |
||||
minMaxIntegers(3, 1); |
||||
} |
||||
@ -0,0 +1,32 @@ |
||||
<?php |
||||
|
||||
use TypePhp\CompilerTest; |
||||
|
||||
final class MinMaxIntegerTest extends BaseTest |
||||
{ |
||||
public function testOnlyTwoProvenIntegerArgumentsBypassZend(): void |
||||
{ |
||||
global $translator; |
||||
|
||||
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||
$translator = $compiler; |
||||
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/min-max-integer.php'; |
||||
$compiler->addFiles([$source]); |
||||
$compiler->prepareFile($source); |
||||
$code = file_get_contents($compiler->convertFile($source)); |
||||
|
||||
self::assertIsString($code); |
||||
$start = strpos($code, 'php::Array php_minmaxintegers('); |
||||
self::assertNotFalse($start); |
||||
$end = strpos($code, 'php::Array php_minmaxfallbacks(', $start); |
||||
self::assertNotFalse($end); |
||||
$integerBody = substr($code, $start, $end - $start); |
||||
self::assertStringNotContainsString('php::call(', $integerBody); |
||||
self::assertSame(13, substr_count($code, 'php::call(')); |
||||
$castStart = strpos($code, 'php::Array php_minmaxcasts('); |
||||
self::assertNotFalse($castStart); |
||||
$castEnd = strpos($code, 'void php_main(', $castStart); |
||||
self::assertNotFalse($castEnd); |
||||
self::assertSame(2, substr_count(substr($code, $castStart, $castEnd - $castStart), 'php::toInt(a)')); |
||||
} |
||||
} |
||||
@ -0,0 +1,25 @@ |
||||
--TEST-- |
||||
Integer min/max evaluates integer casts exactly once per argument |
||||
--FILE-- |
||||
<?php |
||||
class MinMaxWarnings |
||||
{ |
||||
public int $count = 0; |
||||
} |
||||
function main(): void |
||||
{ |
||||
$warnings = new MinMaxWarnings(); |
||||
set_error_handler(static function (int $severity, string $message, string $file, int $line) use ($warnings): bool { |
||||
++$warnings->count; |
||||
return true; |
||||
}); |
||||
$object = new stdClass(); |
||||
var_dump(min((int) $object, 2), max((int) $object, 0)); |
||||
restore_error_handler(); |
||||
var_dump($warnings->count); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(1) |
||||
int(1) |
||||
int(2) |
||||
@ -0,0 +1,66 @@ |
||||
--TEST-- |
||||
Integer min/max optimization preserves values, evaluation order, and dynamic fallback |
||||
--FILE-- |
||||
<?php |
||||
function nextMinMaxValue(int &$value): int |
||||
{ |
||||
++$value; |
||||
return $value; |
||||
} |
||||
function compareMinMax(int $a, int $b): void |
||||
{ |
||||
var_dump(min($a, $b), max($a, $b)); |
||||
} |
||||
function dynamicMinMax(mixed $a, mixed $b): void |
||||
{ |
||||
var_dump(min($a, $b), max($a, $b)); |
||||
} |
||||
function main(): void |
||||
{ |
||||
compareMinMax(8, -3); |
||||
compareMinMax(4, 4); |
||||
compareMinMax(PHP_INT_MIN, PHP_INT_MAX); |
||||
$value = 2; |
||||
var_dump(min($value, (int) nextMinMaxValue($value)), $value); |
||||
var_dump(max(nextMinMaxValue($value), nextMinMaxValue($value)), $value); |
||||
var_dump(min(nextMinMaxValue($value), $value), $value); |
||||
$counter = 0; |
||||
var_dump(false && min(++$counter, ++$counter), $counter); |
||||
var_dump(true ? max(++$counter, ++$counter) : min(++$counter, ++$counter), $counter); |
||||
dynamicMinMax('20', 3); |
||||
dynamicMinMax(false, 2); |
||||
var_dump(min(1, 1.5), max(1, 1.5), min(-2.5, -1.0), max(-1.0, -2.5)); |
||||
var_dump(min([4, 2]), max(...[1, 7, 3]), min(3, 2, 1)); |
||||
$callable = min(...); |
||||
var_dump($callable(4, 2)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(-3) |
||||
int(8) |
||||
int(4) |
||||
int(4) |
||||
int(-9223372036854775808) |
||||
int(9223372036854775807) |
||||
int(3) |
||||
int(3) |
||||
int(5) |
||||
int(5) |
||||
int(6) |
||||
int(6) |
||||
bool(false) |
||||
int(0) |
||||
int(2) |
||||
int(2) |
||||
int(3) |
||||
string(2) "20" |
||||
bool(false) |
||||
int(2) |
||||
int(1) |
||||
float(1.5) |
||||
float(-2.5) |
||||
float(-1) |
||||
int(2) |
||||
int(7) |
||||
int(1) |
||||
int(2) |
||||
@ -0,0 +1,32 @@ |
||||
--TEST-- |
||||
min/max preserve null arguments and nullable property values |
||||
--FILE-- |
||||
<?php |
||||
class MinMaxNullable |
||||
{ |
||||
public ?int $value = null; |
||||
public static ?int $staticValue = null; |
||||
} |
||||
function nullableMinMax(?int $value): void |
||||
{ |
||||
var_dump(min($value, 0), max($value, 0)); |
||||
} |
||||
function main(): void |
||||
{ |
||||
$object = new MinMaxNullable(); |
||||
nullableMinMax(null); |
||||
var_dump(min($object->value, 0), max($object->value, 0)); |
||||
var_dump(min(MinMaxNullable::$staticValue, 0), max(MinMaxNullable::$staticValue, 0)); |
||||
$object->value = 3; |
||||
var_dump(min($object->value, 0), max($object->value, 0)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
NULL |
||||
NULL |
||||
NULL |
||||
NULL |
||||
NULL |
||||
NULL |
||||
int(0) |
||||
int(3) |
||||
@ -0,0 +1,20 @@ |
||||
--TEST-- |
||||
min/max keep overflow-capable varint values on the Zend path |
||||
--FILE-- |
||||
<?php |
||||
use varint_types; |
||||
function main(): void |
||||
{ |
||||
$value = PHP_INT_MAX; |
||||
++$value; |
||||
var_dump(min($value, 2), is_float(max($value, 2))); |
||||
$value = 1; |
||||
$value += 0.5; |
||||
var_dump(min($value, 2), max($value, 2)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(2) |
||||
bool(true) |
||||
float(1.5) |
||||
int(2) |
||||
@ -0,0 +1,28 @@ |
||||
--TEST-- |
||||
Integer min/max optimization respects local functions and imported builtin aliases |
||||
--FILE-- |
||||
<?php |
||||
namespace MinMaxGame { |
||||
use function min as builtinMin; |
||||
use function max as builtinMax; |
||||
|
||||
function min(int $a, int $b): int { return 99; } |
||||
function max(int $a, int $b): int { return -99; } |
||||
function compare(int $a, int $b): void |
||||
{ |
||||
var_dump(min($a, $b), max($a, $b)); |
||||
var_dump(builtinMin($a, $b), builtinMax($a, $b)); |
||||
var_dump(\min($a, $b), \max($a, $b)); |
||||
} |
||||
} |
||||
namespace { |
||||
function main(): void { \MinMaxGame\compare(3, 7); } |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(99) |
||||
int(-99) |
||||
int(3) |
||||
int(7) |
||||
int(3) |
||||
int(7) |
||||
Loading…
Reference in new issue