perf(compiler): group functions for declaration header generation (#129)

master
yavon007 3 weeks ago committed by GitHub
parent 69e2a29ec2
commit 63a4793e0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 55
      benchmark/declaration-headers/README.md
  2. 31
      benchmark/declaration-headers/results-arm64.json
  3. 43
      benchmark/declaration-headers/run.php
  4. 36
      phpunit/src/Build/IncrementalDeclarationTest.php
  5. 9
      src/Generator/DefaultArgumentGenerator.php
  6. 32
      src/Translator.php

@ -0,0 +1,55 @@
# Declaration header generation benchmark
Creates 300 PHP source files with 6,001 functions, each ordinary function
having a default parameter. It prepares and converts the project, then measures
six calls to the real declaration-header generator and discards the first as
warm-up. The timed region includes rendering and writing headers; source
preparation, conversion, and C++ compilation are excluded. This measures one
compiler phase, not total build time.
Run from a checkout with Composer development dependencies installed:
```sh
project_dir=$(mktemp -d)
php benchmark/declaration-headers/run.php "$project_dir" > /tmp/declaration-candidate.log
tail -1 /tmp/declaration-candidate.log
```
The script writes generated sources and build outputs into the supplied
benchmark directory. Reuse the same directory for comparisons: absolute source
paths contribute to header names and therefore the combined content digest.
The baseline does not contain this new benchmark. From the candidate checkout,
these commands create a separate baseline and run the same source in both. The
shared vendor directory is supported by `phpunit/bootstrap.php`'s checkout-local
source loader.
```sh
baseline_dir=$(mktemp -d)
project_dir=$(mktemp -d)
git archive b3898c32 | tar -x -C "$baseline_dir"
ln -s "$(pwd)/vendor" "$baseline_dir/vendor"
mkdir -p "$baseline_dir/benchmark/declaration-headers"
cp benchmark/declaration-headers/run.php "$baseline_dir/benchmark/declaration-headers/run.php"
php "$baseline_dir/benchmark/declaration-headers/run.php" "$project_dir" > /tmp/declaration-baseline.log
php benchmark/declaration-headers/run.php "$project_dir" > /tmp/declaration-candidate.log
tail -1 /tmp/declaration-baseline.log
tail -1 /tmp/declaration-candidate.log
```
Compare median `ms` values and require identical `headers` counts and `sha256`
digests. The digest covers the contents and names of every generated declaration
header, including runtime and aggregate headers.
## Sample result
Linux ARM64 Docker, PHP 8.5.10 ZTS, CLI opcache disabled; baseline `b3898c32`:
| Revision | Median (ms) | Headers |
| --- | ---: | ---: |
| Baseline | 88.001 | 302 |
| Grouped functions | 13.087 | 302 |
The measured phase is about 6.72x faster, with identical output digests. Raw
samples are in `results-arm64.json`. This workload isolates function-heavy
projects; class and constant declaration scans are not optimized by this change.

@ -0,0 +1,31 @@
{
"environment": "Linux ARM64 Docker, PHP 8.5.10 ZTS, no CLI opcache",
"baseline_commit": "b3898c32",
"files": 300,
"functions": 6001,
"baseline": {
"ms": [
88.000792,
87.998334,
90.341888,
90.412138,
87.537832
],
"headers": 302,
"sha256": "bfad6f04ca81c591734aeeaf223cc32ef7064768ca12faba8ac41ef053444f37",
"median_ms": 88.000792
},
"candidate": {
"ms": [
13.015777,
13.490487,
12.763525,
13.087318,
13.106777
],
"headers": 302,
"sha256": "bfad6f04ca81c591734aeeaf223cc32ef7064768ca12faba8ac41ef053444f37",
"median_ms": 13.087318
},
"speedup": 6.7241272810823425
}

@ -0,0 +1,43 @@
<?php
require dirname(__DIR__, 2) . '/phpunit/bootstrap.php';
if ($argc !== 2 || !is_dir($argv[1])) {
fwrite(STDERR, "Usage: php run.php <existing temporary project directory>\n");
exit(1);
}
$root = realpath($argv[1]);
$files = [];
for ($i = 0; $i < 300; ++$i) {
$source = "<?php\n";
for ($j = 0; $j < 20; ++$j) {
$source .= "function f_{$i}_{$j}(int \$value = 1): int { return \$value; }\n";
}
if ($i === 0) {
$source .= "function main(): void {}\n";
}
$file = "$root/file_$i.php";
file_put_contents($file, $source);
$files[] = $file;
}
$translator = $compiler = TypePhp\CompilerTest::create($root);
$compiler->setTargetName('declaration_benchmark');
(new ReflectionMethod($compiler, 'setBuildDir'))->invoke($compiler, "$root/build");
$compiler->addFiles($files);
foreach ($files as $file) {
$compiler->prepareFile($file);
}
$compiler->convert($files);
$generate = new ReflectionMethod($compiler, 'genDeclarationHeaders');
$samples = [];
for ($i = 0; $i < 6; ++$i) {
$start = hrtime(true);
$generate->invoke($compiler, $files);
if ($i > 0) {
$samples[] = (hrtime(true) - $start) / 1e6;
}
}
$hashes = [];
foreach (glob("$root/build/include/*decl.h") as $file) {
$hashes[basename($file)] = hash_file('sha256', $file);
}
ksort($hashes);
echo "BENCHMARK_RESULT=" . json_encode(['ms' => $samples, 'headers' => count($hashes), 'sha256' => hash('sha256', json_encode($hashes))]) . "\n";

@ -122,6 +122,42 @@ PHP);
self::assertSame($this->provider, $symbols['constant:Incremental\\LIMIT']);
}
public function testGroupedDeclarationsPreserveHelpersAndFilesWithoutFunctions(): void
{
file_put_contents($this->provider, <<<'PHP'
<?php
namespace Incremental;
abstract class Base
{
abstract public function amount(int $value = 5): int;
}
function answer(int $value = 42, string ...$labels): int { return $value; }
PHP);
file_put_contents($this->consumer, "<?php\nfunction main(): int { return \\Incremental\\answer(); }\n");
file_put_contents($this->independent, "<?php\nconst MARKER = 1;\n");
$compiler = $this->convertProject();
foreach ([$this->provider, $this->consumer, $this->independent] as $file) {
$expected = $this->invoke($compiler, 'renderFunctionDeclarations', $file)
. $this->invoke($compiler, 'renderDataDeclarations', $file, false, false);
self::assertSame($expected, file_get_contents($compiler->getDeclarationHeaderFile($file)));
}
$provider = file_get_contents($compiler->getDeclarationHeaderFile($this->provider));
self::assertStringContainsString('php_incremental__answer_arg_0_default_value();', $provider);
self::assertStringContainsString('php_incremental__answer_arg_1_default_value();', $provider);
self::assertStringContainsString('php_incremental__base__amount_arg_0_default_value();', $provider);
self::assertStringNotContainsString('php_main(', $provider);
$independent = file_get_contents($compiler->getDeclarationHeaderFile($this->independent));
self::assertStringNotContainsString('php_incremental__answer', $independent);
self::assertStringNotContainsString('php_main(', $independent);
self::assertStringNotContainsString('_default_value', $independent);
$aggregate = $this->invoke($compiler, 'renderFunctionDeclarations');
self::assertStringContainsString('php_incremental__answer(', $aggregate);
self::assertStringContainsString('php_main(', $aggregate);
self::assertStringContainsString('php_incremental__base__amount_arg_0_default_value();', $aggregate);
}
public function testSuperglobalsAreDeclaredInEachUsingSource(): void
{
file_put_contents($this->provider, <<<'PHP'

@ -11,6 +11,7 @@ use TypePhp\Type;
use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\ArrayInitPlan;
use TypePhp\Entity\FunctionDef;
trait DefaultArgumentGenerator
{
@ -43,13 +44,11 @@ trait DefaultArgumentGenerator
return "do {\n" . $plan->init . $body . $plan->clean . "} while (0);\n";
}
protected function genDefaultArgumentHelperDeclarations(?string $sourceFile = null): string
/** @param array<string, FunctionDef> $functions */
protected function genDefaultArgumentHelperDeclarations(array $functions): string
{
$code = '';
foreach ($this->symbols->functions() as $nativeName => $func) {
if ($sourceFile !== null && $func->sourceFile !== $sourceFile) {
continue;
}
foreach ($functions as $nativeName => $func) {
foreach ($func->argInfoList as $argumentIndex => $argInfo) {
if (!$this->shouldGenerateDefaultArgumentHelper($argInfo)) {
continue;

@ -2841,8 +2841,15 @@ CODE;
$this->writeFile($file, $this->renderFunctionDeclarations());
}
protected function renderFunctionDeclarations(?string $sourceFile = null): string
/** @param array<string, FunctionDef>|null $functions */
protected function renderFunctionDeclarations(?string $sourceFile = null, ?array $functions = null): string
{
$functions ??= $sourceFile === null
? $this->symbols->functions()
: array_filter(
$this->symbols->functions(),
static fn (FunctionDef $function): bool => $function->sourceFile === $sourceFile,
);
$code = '#pragma once' . PHP_EOL . PHP_EOL;
$code .= '#include <phpx.h>' . PHP_EOL;
$code .= '#include <typephp_helper.h>' . PHP_EOL;
@ -2861,10 +2868,7 @@ CODE;
$code .= $this->genLibraryApiMacro($this->targetName);
}
$importLibraries = [];
foreach ($this->symbols->functions() as $function) {
if ($sourceFile !== null && $function->sourceFile !== $sourceFile) {
continue;
}
foreach ($functions as $function) {
if ($this->isImportedFunction($function)) {
$importLibraries[$function->importLibrary] = true;
}
@ -2873,12 +2877,9 @@ CODE;
$code .= $this->genLibraryImportMacro($library);
}
$code .= $this->genDefaultArgumentHelperDeclarations($sourceFile);
$code .= $this->genDefaultArgumentHelperDeclarations($functions);
foreach ($this->symbols->functions() as $name => $func) {
if ($sourceFile !== null && $func->sourceFile !== $sourceFile) {
continue;
}
foreach ($functions as $name => $func) {
if ($func->abstractMethod) {
continue;
}
@ -3008,11 +3009,20 @@ CODE;
$this->writeFile($runtimeHeader, '#pragma once' . PHP_EOL . PHP_EOL
. $this->renderDataDeclarations(null, true, false)
. $this->genNativeObjectForwardDeclarations());
$functionsBySource = null;
foreach ($this->declarationHeaderFiles as $file => $header) {
if (!$this->shouldRegeneratePhpFile($file)) {
continue;
}
$code = $this->renderFunctionDeclarations($file);
// Group once, only if a header needs regeneration. Keep this local
// so later passes see symbols finalized by their own conversion.
if ($functionsBySource === null) {
$functionsBySource = [];
foreach ($this->symbols->functions() as $name => $function) {
$functionsBySource[$function->sourceFile][$name] = $function;
}
}
$code = $this->renderFunctionDeclarations($file, $functionsBySource[$file] ?? []);
$code .= $this->renderDataDeclarations($file, false, false);
$this->writeFile(
$this->getIncludeDir() . '/' . $header,

Loading…
Cancel
Save