Skip to content

Build smaller turbo binaries - #6552

Merged
ondrejmirtes merged 6 commits into
2.3.xfrom
turbo-binary-diet
Sep 23, 2026
Merged

ondrejmirtes merged 6 commits into
2.3.xfrom
turbo-binary-diet

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

The turbo binaries had grown to ~12 MB each (369 MB across the 36 in phpstan/phpstan, a ~140 MB Composer zip). This takes the Linux .so from 12.0 MB to 8.3 MB (6.3 MB stripped, the shipped form) and makes it 4.1% faster than today's CI build; macOS goes from 10.5 MB to 7.1 MB stripped at unchanged speed.

Commits

  • Encode the generated turbo signatures without pointers: every pointer in a constant table is a load-time relocation (24 bytes of .rela.dyn on ELF plus its slot). The generated reg::Arg/reg::Sig records were 27,157 of 30,374 relocations. Each generated header now has one string table and one parameter table (sigtab); the records hold 16-bit offsets, and reg::Sig carries the tables as template arguments, so cls.method(sigs::x, ...) call sites are unchanged. Relocations 30,374 → 2,995, .rela.dyn 736 KB → 78 KB.
  • Mark the turbo registration functions cold: they run once at startup.
  • Build smaller turbo binaries:
    • make strip in phar.yml, before signing and before the tests;
    • no unwind tables;
    • section GC;
    • LTO under GCC;
    • an own terminate handler in the static-libstdc++ build, so libstdc++'s demangler stays out.
  • Initialize the zval …: a GCC 11 -Wmaybe-uninitialized false positive in the instrumented PGO stage under the new flags.
  • Load turbo through PHP_INI_SCAN_DIR in the benchmark protocol: php -d extension=… bin/phpstan is silently dropped by TurboProcessRestarter's OPcache restart, so such runs measure PHP alone (40.0 s vs 17.2 s user here).

Measurements

User CPU on src/Type src/Analyser --debug, 11 interleaved pairs, extension loaded via PHP_INI_SCAN_DIR:

Linux arm64 (GCC 11.4) macOS arm64 (clang)
PGO (kept) −4.0% (t=−6.3) −3.6% (t=−14.1)
this branch vs 2.3.x, without PGO −1.3% (noise) −0.3% (noise)
LTO on top of PGO −3.9%, 15% smaller → adopted −1.2%, 9% larger → not adopted
final vs CI today −4.1% −0.2%
-Os + LTO +10.4% → rejected —

Stripped sizes: Linux 8.91 → 6.28 MB, macOS 7.16 → 7.06 MB.

Verified on both platforms with the CI build (strict warnings, make pgo, make strip, signing on macOS):

  • signature-parity: identical for all 14,058 methods;
  • smoke, arena, trusted-types, both parser corpora: pass;
  • walk-trace: identical;
  • side-by-side: 351 classes paired;
  • --error-format=raw output: byte-identical with turbo on and off.

Not verified locally: Windows (untouched; the DLL was already stripped) and Alpine GCC 15 with LTO.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qi7GRGMn9ZuY2jSc97wqBF

ondrejmirtes and others added 6 commits September 23, 2026 09:49
…ag in

GCC 11.4's instrumented PGO stage reports it as maybe-uninitialized once
the extension is built with per-function sections, which -Werror turns
into a build failure. Initializing flagged locals is the convention.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qi7GRGMn9ZuY2jSc97wqBF
Every pointer in a constant table is a load-time relocation: 24 bytes of
.rela.dyn on ELF plus its 8-byte slot. The generated reg::Arg and
reg::Sig records were 27,157 of the Linux .so's 30,374 relocations.

Each generated header now has one string table and one parameter table
(namespace sigtab); the records hold 16-bit offsets and indexes into them,
and reg::Sig carries the two tables as template arguments, which compile
to PC-relative addresses at the registration sites, so cls.method(sigs::x,
...) stays as it was. Linux arm64 (GCC 11.4, -O2): relocations 30,374 ->
2,995, .rela.dyn 736 KB -> 78 KB, .data.rel.ro 506 KB -> 27 KB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qi7GRGMn9ZuY2jSc97wqBF
They run once per process at module startup; ZEND_COLD makes the compiler
optimise their bodies for size and move them out of the hot text. The
method handlers they register are separate functions whose addresses
escape, and keep their optimisation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qi7GRGMn9ZuY2jSc97wqBF
When the OPcache settings are not the ones TurboProcessRestarter wants
(OPcache off for CLI in the CI images, validated timestamps on a default
Homebrew PHP), PHPStan re-executes itself and a command-line
-d extension= does not survive: the run measures PHP alone while
php -d extension=... -r still reports the extension's version. On
src/Type src/Analyser --debug: 40.0 s user with -d, 17.2 s through
PHP_INI_SCAN_DIR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qi7GRGMn9ZuY2jSc97wqBF
The shipped .so had grown to ~12 MB per platform and PHP version, 369 MB
across the 38 binaries in phpstan/phpstan. Measured on Linux arm64 (GCC
11.4, make pgo), stripped:

- strip the symbol tables in CI (make strip, before signing and before the
  tests, so they run the shipped binary): 12.0 MB raw -> 8.9 MB
- no unwind tables: the extension never throws and never walks its own
  stack, yet -fno-exceptions alone still leaves .eh_frame (0.8 MB)
- per-function sections with --gc-sections / -dead_strip
- LTO under GCC: 3.9% faster on top of PGO and 15% smaller; clang's full
  LTO was 1.2% faster but 9% larger, so macOS builds stay without it
- main.cpp supplies the terminate handler in the static-libstdc++ build,
  keeping libstdc++'s demangler out of the link

With the pointer-free signatures and cold registration, the Linux .so
goes from 12.0 MB to 8.3 MB raw and 6.3 MB stripped, 4.1% faster than
today's CI build; macOS from 10.5 MB to 7.1 MB stripped at unchanged
speed. -Os was measured and rejected: a third smaller than -O2, but 10.4%
slower. PGO keeps its place: 4.0% faster on Linux, 3.6% on macOS. The
sanitize target restores the unwind tables its stack traces need.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qi7GRGMn9ZuY2jSc97wqBF
@ondrejmirtes
ondrejmirtes merged commit 809098d into 2.3.x Sep 23, 2026
529 of 533 checks passed
@ondrejmirtes
ondrejmirtes deleted the turbo-binary-diet branch September 23, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant