Skip to content

Release: build-time extension selection, and measure the Phase 2.5 acceptance test - #24421

Merged
PurHur merged 1 commit into
masterfrom
release/extension-selection
Jul 28, 2026
Merged

PurHur merged 1 commit into
masterfrom
release/extension-selection

Conversation

@PurHur

@PurHur PurHur commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Adds --only= / --without= to the registry generator — and then runs the acceptance test the
plan names
, which changes what this phase is for.

The measurement

RELEASE-PLAN Phase 2.5 states: "a script that never calls into an extension pays nothing for it —
not in link time, not in binary size, not in startup."
I regenerated the registry with 6 extensions
instead of 76 and rebuilt hello world:

registry hello-world binary cold build
all 76 extensions 17,148,584 B 6 s
6 extensions 17,148,944 B 6 s

The minimal build is 360 bytes larger. No saving at all.

Two further measurements explain it:

  • nm finds zero curl/mongodb/snmp/ldap symbols in a hello-world binary built with the
    full 76-extension registry — unused extensions are never linked in;
  • a script that does call an extension grows the binary ~572 KB (bcadd) — so demand-driven
    linking already works.

The binary is dominated by .text at ~15 MB: runtime and compiler core, not extensions.

What this means for the phase

Phase 2.5 cannot be justified by user binary size or user build time — that benefit already
exists.
The plan sizes the phase at "37% fewer files", which implies a proportional user-visible
win. Measured, it is not there to collect.

What survives, and is worth doing:

  1. Extensibility — "a new extension is a directory, not a core edit". Delivered by Release: generate the extension load list instead of hardcoding it (Phase 2.5) #24418.
  2. The compiler's own build cost — the 6,519-file spine as a single translation unit, which is
    the multi-hour gen-0 problem. That is the real cost centre, and it is what the per-module TU split
    addresses.

I would rather report this now than spend weeks moving module boundaries for a saving that does not
exist. The plan is updated in place so the next person does not re-derive it.

The selection mechanism

--only= / --without=, with a dependency closure so a selected extension never silently loses
one:

$ php script/generate-extension-registry.php --only=standard,spl,dom
generate-extension-registry: kept for declared dependencies: libxml
generate-extension-registry: wrote lib/ExtensionRegistry.php (4 extensions)

Selection is at generation time, not runtime, and that is forced by the architecture: the
registry emits literal new expressions that the AOT compiler resolves statically, so a referenced
module is compiled in regardless of any runtime filter. Dropping the cost means dropping the
reference.

Identity is the ext/ directory, never getExtensionName() — 20 modules report 'standard'
(#24418).

Gate

No behaviour change: lib/ExtensionRegistry.php regenerates identical (76 extensions).

generate-extension-registry --check   ok (76 extensions)
check-extension-dependencies          ok — 6 extensions, 7 edges
ExtensionRegistryOrderTest            OK (2 tests, 231 assertions)
aot-smoke                             8/8
cold-build-check                      ok — 6s

No sweep run: this changes a generator and a doc, and the generated file is byte-identical to the
committed one.

Aside, not chased

The bcadd probe binary produced no output where it should print 3.3. I could not judge that
against Zend — this image's PHP has no bcmath, so php and bin/vm.php both fatal with "Call to
undefined function bcadd()"
while the compiler implements its own. Flagging rather than filing,
since the corpus cannot gate a function Zend does not have here.

…ceptance test

Adds --only= / --without= to script/generate-extension-registry.php, with a dependency closure so a
selected extension never silently loses one (--only=standard,spl,dom keeps libxml automatically, and
says so). Identity is the ext/ DIRECTORY, never getExtensionName() — 20 modules report 'standard'.

Selection is at GENERATION time, not runtime, and that is forced by the architecture: the registry
emits literal `new` expressions which the AOT compiler resolves statically, so a referenced module is
compiled in regardless of any runtime filter. Dropping the cost means dropping the reference.

Then ran the acceptance test the plan names — "a script that never calls into an extension pays
nothing for it" — instead of assuming it:

  registry            hello-world binary   cold build
  all 76 extensions   17,148,584 B         6 s
  6 extensions        17,148,944 B         6 s     <- 360 bytes LARGER, i.e. no change

Two measurements explain it:

  - nm finds ZERO curl/mongodb/snmp/ldap symbols in a hello-world binary built with the full
    76-extension registry. Unused extensions are not linked in at all.
  - a script that DOES call an extension grows the binary ~572 KB (bcadd), so demand-driven linking
    already works.

The binary is dominated by .text at ~15 MB — runtime and compiler core, not extensions.

CONSEQUENCE, recorded in the plan: Phase 2.5 must not be justified by user binary size or user build
time, because that benefit already exists. What survives is (1) extensibility — a new extension is a
directory, not a core edit, delivered by the generated registry in #24418 — and (2) the compiler's
OWN build cost, the 6,519-file spine as a single translation unit, which is the multi-hour gen-0
problem the per-module TU split addresses.

This matters because the plan sized this phase at "37% fewer files" and implied a proportional win
for users. Measured, that win is not there to collect, and weeks aimed at it would have produced no
user-visible change.

No behaviour change: lib/ExtensionRegistry.php is regenerated identical (76 extensions,
--check ok), check-extension-dependencies ok, ExtensionRegistryOrderTest 2 tests / 231 assertions ok.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PurHur
PurHur merged commit a9b37a5 into master Jul 28, 2026
@PurHur
PurHur deleted the release/extension-selection branch July 29, 2026 12:22
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.

2 participants