9.0 KiB
AOT Compilation Speed Optimization Research Notes
This document records the current assessment of Swoole-Compiler AOT compilation speed, bottleneck analysis, and future research directions. It is not yet tied to any specific PR.
Goal
Reduce the following two categories of time cost:
- Cold-start full build:
./bin/tpc.php project.yml - Hot-start incremental build: recompiling after changing only a few PHP files
The focus is on large projects and compiler self-hosting scenarios.
Current Pipeline
The main flow is in src/Php/Translator.php:
prepare(): scan files, parse AST, collect symbols, sort dependenciesconvert(): generate the corresponding.ccfor each PHP filegenStubFile(): generate the arginfo / class register header filegenFunctionDeclarations()/genDataDeclarations(): generate the build-time internal declaration headergenExtension(): generate a singleextension-<target>.cccompile(): compile all.cc/.c/...into.obuild(): link into the final executable or extension
Main Bottleneck Assessment
1. Common Header Churn Causing Full Recompilation
Currently all translation units include:
php_<target>_func_decl.hphp_<target>_data_decl.h
As soon as any function declaration, default-argument helper, or global symbol declaration changes, a large number of .cc files get recompiled.
This is one of the key reasons for the poor incremental build performance of large projects.
2. The extension-<target>.cc Single File Is Too Large
The extension main file carries:
- class entry registration
- the function table
- literal strings
- module initialization
- static property initialization
- constant initialization
The larger the project, the larger this single TU becomes, making it easy to become a compilation tail bottleneck; even if other files can be compiled in parallel, they all stall on this one large file.
3. Missing a General Incremental Cache
Currently only phpx/src/misc has an object cache:
hasMiscObjectFileCache()
The .cc, common headers, arginfo headers, and extension file generated for the user's own project are still essentially fully regenerated and fully recompiled.
4. clang-format Overhead Is Fixed and Serial
formatCppCode() runs once for each generated file:
clang-format -i <file>
This introduces:
- extra process startup overhead
- a large amount of disk I/O
- serial formatting waits
It is especially noticeable for large projects.
5. arginfo / stub Are Regenerated Every Time
generateStubFile() currently runs every time; even if the input PHP files have not changed, it regenerates the header files, further amplifying the header churn problem.
6. Only the Compilation Stage Is Parallel; the Front Stages Are Mostly Serial
Currently compileWithPcntl() only parallelizes .cc -> .o:
- prepare
- convert
- stub generation
- format
These stages are still mostly serial.
Highest-priority Optimization Directions
P0: Do Not Rewrite Files When Content Is Unchanged
This is the most worthwhile foundational change to prioritize.
Idea
For all generated files:
.cc- arginfo
.h php_<target>_func_decl.hphp_<target>_data_decl.hextension-<target>.cc
Compare the content before writing to disk:
- Same content: do not write the file
- Different content: write it
Value
Avoid triggering downstream full recompilation merely because of an mtime change.
P0: General object cache / incremental compilation
Extend the current caching approach that only targets phpx/src/misc to user-generated code.
Suggested Cache Conditions
For each target .o:
.oexists.ois newer than its corresponding source file.ois newer than the headers it depends on- The compile option signature has not changed (optimization level, debug, sanitize, cxxflags, PHP/ZTS, etc.)
When satisfied, skip compilation directly.
Supporting Requirements
A clear "build signature" mechanism is needed, for example:
- compiler backend
- cpp compiler path
- C++ standard
- optimize/debug/sanitize
- build mode
- PHP/ZTS information
P0: Disable clang-format by Default
It is recommended to make formatting an explicit capability rather than part of the default compilation path.
Recommendation
- Disable by default
- Add a
--formator debug/dev mode to enable it - Or format only changed files
Value
This is a low-risk optimization with immediate effect.
P1: Split the Common Declaration Header
Current Problem
Complex default-argument helpers also enter the common func_decl.h, widening the impact of header changes.
Optional Directions
- Split declaration headers by source file
- Move helpers from the common header to local headers / local
.cc - Only truly cross-TU declarations go into the common header
Goal
Reduce "one change, full project recompilation".
P1: Split extension-<target>.cc
Splittable Modules
extension-main.ccextension-class-register-*.ccextension-function-table.ccextension-const-init.ccextension-static-init.cc
Value
- Reduce the size of a single TU
- Enhance parallel compilation benefits
- Reduce the tail wait for large projects
P1: arginfo / stub caching
Direction
Introduce input-content-based caching for generateStubFile():
- Source PHP content hash
- gen_stub version signature
- PHP version signature
Do not overwrite output header files when the content is unchanged.
Value
Reduce header churn, with a clear effect when combined with incremental builds.
P2: Parallelizing prepare / convert / stub generation
Currently only the compile stage is parallelized. Later the following can be explored:
- Layering by dependency topology after file scanning
- Parallel convert for files in the same layer
- Parallel stub generation for files in the same layer
Risk Points
- There is a lot of shared state (literalStrings, classMap, funcMap, propMap, symbol tables, etc.)
- It is necessary to first sort out which state can be sharded and which must be merged
Therefore this direction has large benefits but also higher implementation complexity.
P2: Symbol-dependency-driven minimal recompilation
An ideal incremental build should not be based only on file timestamps, but on:
- which symbols have changed
- which files depend on those symbols
Goal
When modifying one PHP file, rebuild only:
- the file itself
- files that depend on its exported symbols
- the necessary extension / declaration modules
This would significantly improve hot build speed for large projects.
P2: Toolchain-level optimization
Compilation caches
ccachesccache
Faster linkers
moldlld
Precompiled headers
Try PCH for stable large headers, for example:
phpx.hphpx_helper.hphpx_std.h
These optimizations are relatively cheap to implement and can be advanced together with the compiler option layer.
Special Constraints on Literal Arrays
Literal arrays are different from literal strings:
- Literal strings can leverage permanent strings to bypass the Zend request lifecycle
- Literal arrays must exist from
module_init()tomodule_clean(), i.e. between PHP'sRINIT/RSHUTDOWN
Therefore all future "array initialization caching" research must obey:
- PHP arrays must not be persisted into process-level permanent objects
- Only the "initialization plan" or "generated code template" can be cached
- Real array objects must be constructed within the request lifecycle
The already-introduced ArrayInitPlan belongs to this kind of safe abstraction:
- It only saves
expr/init/clean - It does not save array object instances that cross requests
Suggested Landing Order
Phase One (Fastest Effect)
- Do not write files when content is unchanged
- General
.ocache - Disable clang-format by default
- arginfo/stub content cache
Phase Two (Structural Benefits)
- Split the common header
- Split
extension-<target>.cc - Narrow the visibility of default-argument helpers
Phase Three (Long-term Optimization)
- prepare/convert parallelization
- Symbol-dependency-driven minimal recompilation
- PCH / ccache / mold / sccache
Suggested Code Locations to Research First
src/Php/Translator.phpformatCppCode()compile()compileSourceFile()compileWithPcntl()genFunctionDeclarations()genDataDeclarations()genExtension()genStubFile()
src/Php/Backend/*- Compile/link command construction, convenient for integrating
ccache/mold/lld
- Compile/link command construction, convenient for integrating
A Realistic Assessment
For large projects, slow AOT compilation is usually not simply "g++ is slow", but the superposition of the following:
- Full regeneration
- Header churn causing full recompilation
- A single oversized extension TU
- Per-file formatting
- Lack of a real incremental cache
Therefore the most effective direction is not to first tune compilation flags, but to prioritize:
- incrementality
- splitting
- reducing the common dependency surface