Repository navigation
[3.x] Speed up compiling integer literals - #5003
Open
andreybolonin wants to merge 1 commit into
Open
andreybolonin wants to merge 1 commit into
andreybolonin wants to merge 1 commit into
Conversation
stof
approved these changes
Oct 19, 2026
andreybolonin
force-pushed
the
speed-up-compiler-integer-repr
branch
from
October 19, 2026 18:07
eaf759d to
50e4651
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Compiler::repr()currently queriesLC_NUMERIC, switches it toC, and restores it for every integer as well as every float. Integer representations do not need locale switching. Handle integers in a separate branch and retain the existing locale andserialize_precisionhandling for floats.Keep using
var_export()for integers. In particular, its special representation ofPHP_INT_MINmust remain intact so evaluating the generated PHP produces an integer without losing precision. The now float-only branch also no longer needs to checkis_float()again before settingserialize_precision.Measurements
Benchmarked the proposed branch before making the repository change, then repeated the measurements against the exact final patch. The figures below are from the final patch, compared with
428a16179.Environment: PHP 8.5.11 CLI, macOS arm64, JIT disabled, tested with CLI OPcache both disabled and enabled. Each figure is the median of 9 batches, alternating before/after execution order, after 15 warm-up calls per version. Both complete compiler implementations are loaded as separate subclasses with the same inheritance structure in one process, so the candidate is not measured through an extra overriding wrapper. Times are microseconds per call, not batch totals.
Isolated
repr()calls, includingreset()before each call:123, C localerange(0, 99), C locale0.1, C localehello, C locale123, French localeNegative percentages mean less elapsed time. The 100-element array exercises integer keys as well as integer values. Float and string rows are controls; small differences on those paths should not be read as a separate performance claim.
Full
Environment::compileSource(), including lexing, parsing, node visitors, and code generation, with a reused environment and warmed compiler/parser/lexer:{% set x = [1, 2, 3, 4, 5] %}{{ x[0] }}Repeat this 40-byte line (including its trailing newline) 1, 10, 100, or 1,000 times. Each line has six integer literals in the Twig source. The zero-assignment control is the 13-byte static template
Hello, world!.CLI OPcache disabled:
CLI OPcache enabled:
The isolated integer path consistently takes about half the time. Full compilation benefits are much smaller because lexing, parsing, and traversal dominate; larger-template results are also more sensitive to run-to-run variation. These measurements concern compilation of the specified numeric workload, not rendering an already compiled/cached template or a universal 2% improvement for all templates. Complete batch ranges are included below.
Behavior and validation
serialize_precisionvalues −1, 0, and 17. Locale and precision are preserved after each call.PHP_INT_MIN,PHP_INT_MIN + 1, andPHP_INT_MAX; extended locale/precision tests to cover integers alongside floats.vendor/bin/simple-phpunit: 2,872 tests, 7,026 assertions, 4 skipped, no failures (99 existing legacy deprecation notices reported).vendor/bin/phpstan analyse --no-progress: no errors.vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --dry-run --diff src/Compiler.php tests/CompilerTest.php: no changes needed.Raw benchmark results, including batch ranges
CLI OPcache disabled:
CLI OPcache enabled:
Reproduce the benchmark and output/state comparisons
Check out this branch and install Composer dependencies. Save the PHP script below as
/tmp/twig-repr-benchmark.php, then run from the repository root:git show 428a16179:src/Compiler.php > /tmp/twig-compiler-before.php php -d opcache.enable_cli=0 /tmp/twig-repr-benchmark.php php -d opcache.enable_cli=1 -d opcache.file_update_protection=0 /tmp/twig-repr-benchmark.phpBoth class variants are loaded from files. Disabling OPcache's file update protection for the second command lets it optimize both newly generated variant files equally. Both C and
fr_FR.UTF-8numeric locales are needed for the state comparisons and French-locale measurements. Measurements should be run without other CPU-intensive work.Memory measurements
Compared
428a16179with this MR's exact head in separate, fresh PHP processes (not two renamed classes loaded together). PHP 8.5.11 CLI on macOS arm64, JIT disabled, with CLI OPcache both off and on. Five independent processes per version/workload/OPcache mode, alternating execution order; values below are medians.Inputs, the environment, and extension initialization are set up before the measured interval.
memory_reset_peak_usage()resets the peak immediately before the operation. Reported used-heap peaks come frommemory_get_peak_usage(false); reserved peaks come frommemory_get_peak_usage(true). Absolute peaks include the existing baseline heap; “incremental peak” subtracts the heap immediately before the operation. Result objects/strings remain alive when memory is sampled. Output verification happens afterward, so its serialization buffers are excluded. Process max RSS includes startup/setup and is sampled before verification. OPcache shared usage is reported separately because it is not part of the PHP used-heap metric.All before/after output hashes matched. These are memory measurements only; process startup is excluded from the earlier time benchmarks.
Full
compileSource(), using the same 40-byte numeric-assignment line as in the time benchmark; the zero-assignment control isHello, world!:The incremental peak of compilation is identical before/after at every size in both OPcache modes. Without OPcache, the starting PHP heap is 4,096 B higher after the change, so absolute compilation peaks are also 4,096 B higher. An independent check loading only the Compiler class measured 24,688 → 28,784 B of used PHP heap for the class definition, confirming this is the fixed cost of the modified class rather than per-template allocation. With OPcache enabled, the starting PHP heap and absolute compilation peaks are identical; OPcache-reported shared used memory is 96 B higher after the change.
repr()alone, includingreset(), with the integer input/array constructed before resetting the peak:123The incremental peaks in this table are the same with OPcache off/on. The integer path uses 32 B less temporary heap, with unchanged retained output memory. This does not constitute a meaningful memory-saving claim.
After a warm-up followed by 100 compilations of a 4,000-byte numeric template, retained heap growth after garbage collection is 0 B in both versions, and the incremental peak is identical. Peak reserved PHP memory is unchanged in every tested workload.
Process max RSS for the largest full-operation workload (includes startup/setup; five fresh processes per side):
RSS has page-level/process-startup variation and should not be used to attribute byte-sized differences to this patch; the allocator/retained-heap figures above show the allocation differences directly.
Reproduce the memory benchmark
Install Composer dependencies, save the PHP and Python scripts below as
/tmp/twig-memory-probe.phpand/tmp/twig-memory-run.py, then run from the repository root:The runner launches 420 fresh PHP processes in total across both MRs. It loads the original class name from the selected snapshot before the Composer autoloader can load it, so each process contains exactly one implementation of the measured class. All other source files must match the MR base; the time/memory changes in these two MRs are confined to the selected Compiler/Lexer methods. Inputs and dependencies outside those classes are shared identically across each comparison. PHP 8.2+ is needed for
memory_reset_peak_usage(); the reported results use PHP 8.5.11. The scripts save all individual measurements to/tmp/twig-memory-results.jsonand print per-case medians.