Skip to content

perf(wasm): tune inlining with link-time optimization - #135

Draft
BridgeAR wants to merge 1 commit into
mainfrom
BridgeAR/2026-10-06-cjs-wasm-lto
Draft

BridgeAR wants to merge 1 commit into
mainfrom
BridgeAR/2026-10-06-cjs-wasm-lto

Conversation

@BridgeAR

@BridgeAR BridgeAR commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Link-time optimization and selective inlining reduce median warm parsing CPU by 12.7%, 8.8%, and 12.5% for small, medium, and large application modules. The control uses default flags. The binary grows from 22,639 to 25,277 bytes.

Measurements use Node 22.23.4/V8 12.4 on macOS arm64 and five fresh processes. Each process has 50,000 warmup calls per variant and seven interleaved batches with trimmed CPU means. Median cold import, initialization, and first-file parsing measures 1.851 ms with default flags and 1.963 ms with this build.

@guybedford

Copy link
Copy Markdown
Collaborator

Thanks for this. I rebuilt the binary from the pinned wasm-builder container and it is byte-identical to the committed lib/lexer.wasm, and all tests pass in all three modes.

The perf result doesn't reproduce on x86_64 Linux though (Node 22.21.1, isolated processes, trimmed mean of 7 batches, test/samples inputs). I also split the two flags to see which one is responsible:

input base PR (-flto + threshold 150) -flto only threshold 150 only
exports.a = 1; module.exports.b = 2; 0.568us 0.505 (-11%) 0.575 (0%) 0.550 (-3%)
rollup.min.js 1996us 1880 (-6%) 1946 (-2.5%) 1890 (-5%)
lexer.js 154us 161 (+4%) 154 (0%) 167 (+8%)
magic-string.js 99us 119 (+19%) 98 (0%) 115 (+16%)

angular / d3 / rollup unminified were -1% to -3%.

So -flto on its own is perf-neutral and shrinks the binary (22369 -> 22057 bytes), while all of the size growth and all of the perf delta, both the wins and the regressions, come from -inline-threshold=150. A global inline threshold is going to be sensitive to the host JIT's register allocation and code layout per architecture, which is consistent with it helping on arm64 and hurting the hot scanning loops for unminified mid-sized files on x86_64.

Suggestions:

  • Keep -flto.
  • Drop the global -inline-threshold=150, or back it with x86_64 Linux numbers showing no regressions across the sample set.
  • If specific call sites matter, targeted __attribute__((always_inline)) / noinline in src/lexer.c would be deterministic across platforms and easier to review than a global heuristic.

Minor: with -flto the link-time -Wl,--mllvm=-inline-threshold is the one that drives the main pipeline, so the compile-time -mllvm copy is mostly redundant.

@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-10-06-cjs-wasm-memory branch from f45d1a0 to 07b764e Compare October 11, 2026 09:33

@guybedford guybedford left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and for the careful measurements. I rebuilt the binary with the pinned toolchain (build-wasm-local, wasi-sdk 21 / binaryen 116) and it is byte-identical to the committed lib/lexer.wasm, and the full suite passes in all three modes.

I did an interleaved A/B of flag variants on the same src/lexer.js, Node 22.21 / V8 12.4, Linux x86_64. Medians over 60 shuffled rounds after warmup, all after wasm-opt -Oz:

Build Size Large samples Medium (~3 KB) Small (~150 B)
-Oz (control) 22401 0% 0% 0%
-Oz -flto -mllvm -inline-threshold=150 (this PR) 25065 -2% -5.5% -7.6%
-Oz -mllvm -inline-threshold=150 (no LTO) 25522 -1 to -2%
-Oz -flto only 22057 within noise
-O2 27121 -12 to -14% -10% -12%
-O3 27894 -13% -10% -14%

Cold start (import + init + first parse in fresh processes) was within noise across all of them, about 0.1 ms spread.

Two things fall out of this:

  • -flto on its own does nothing here, which is expected for a single translation unit. The whole effect comes from the inline threshold.
  • -mllvm -inline-threshold is an internal LLVM option without a stability guarantee across clang versions, while plain -O2 is a stable public flag and gave roughly twice the speedup for another ~2 KB.

So the real decision seems to be how much inlined binary size the loader is willing to pay, and if some is acceptable, -O2 looks like the better point on that curve. Could you run -O2 through your arm64 harness alongside the current flags? If it holds up there too, I'd prefer that over the LTO / threshold pair.

Separately, the stack is based on 2.3.0 and the src/lexer.js change in #134 conflicts with #131 on main, so both will need a rebase before landing.

Comment thread build/Makefile
# These are project-specific and are expected to be kept intact
WASM_TARGET := -target wasm32-unknown-wasi
WASM_EXTRA_CFLAGS := -I include-wasm/ -Wno-logical-op-parentheses -Wno-parentheses -Oz
WASM_EXTRA_CFLAGS += -flto -mllvm -inline-threshold=150

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a single translation unit -flto had no measurable effect on its own in my runs (see the table in the review body); the gain is entirely from the inline threshold. If the threshold approach stays, this could be dropped along with the -Wl,--mllvm duplicate below, but see the review for the -O2 comparison.

Link-time optimization and selective inlining reduce median warm parsing
CPU by 12.7%, 8.8%, and 12.5% for small, medium, and large application
modules. The control uses default flags. The binary grows from 22,639
to 25,277 bytes.

Measurements use Node 22.23.4/V8 12.4 on macOS arm64 and five fresh
processes. Each process has 50,000 warmup calls per variant and seven
interleaved batches with trimmed CPU means. Median cold import,
initialization, and first-file parsing measures 1.851 ms with default
flags and 1.963 ms with this build.

Signed-off-by: Ruben Bridgewater <ruben.bridgewater@datadoghq.com>
@BridgeAR
BridgeAR changed the base branch from BridgeAR/2026-10-06-cjs-wasm-memory to main October 25, 2026 12:38
@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-10-06-cjs-wasm-lto branch from 555cd7f to 7cabdf3 Compare October 25, 2026 12:38
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