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