Skip to content

experiment: try LTO for Linux native builds (ELF only)#72

Closed
dfa1 wants to merge 1 commit into
mainfrom
experiment/lto-linux
Closed

experiment: try LTO for Linux native builds (ELF only)#72
dfa1 wants to merge 1 commit into
mainfrom
experiment/lto-linux

Conversation

@dfa1

@dfa1 dfa1 commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Experimental follow-up to #70 / #71: enable -flto for the linux-x86_64/linux-aarch64 classifiers only.

  • Mach-O is out of scope here — zig's Mach-O backend is a self-hosted linker with no LTO support (error: LTO requires using LLD, reproducible even at the -c compile stage, no workaround found — no real ld64.lld locally, zig cc exposes no linker-selection flag that helps).
  • ELF is different: zig 0.16 still links ELF through its bundled real lld by default (the self-hosted ELF linker is early/opt-in and, per the 0.16 devlog, can't even link external-library code yet — not usable for us regardless). -flto compiles and links cleanly when cross-compiled locally to x86_64-linux-gnu/aarch64-linux-gnu, and the full public API surface survives (checked with llvm-readelf --dyn-symsZSTD_compress, ZSTD_decompress, ZSTD_createCCtx, ZDICT_trainFromBuffer, etc. all still exported, ~198 public symbols).

Why this is a separate PR from #71 and marked experimental: everything in #71 was verified end-to-end on this (macOS) host, including running the actual test suite against the built library. This one I can't fully verify locally — this Mac can't execute Linux ELF binaries, so linking without error is as far as local verification goes. zstd/pom.xml / integration-tests/pom.xml activate their native-classifier Maven profile by host OS/arch, so ci.yml's ubuntu-latest job will natively build and test-run exactly the linux-x86_64 classifier this PR touches — that's the real correctness signal I'm after here. If CI is green, this is solid; if not, we'll know exactly where it breaks.

osx-*/windows-* are untouched (confirmed: rebuilt osx-aarch64, full zstd/integration-tests suite still passes; all six classifiers still cross-compile cleanly).

Test plan

  • Cross-compiled linux-x86_64/linux-aarch64 locally with -flto, both .o compile and .so link succeed
  • llvm-readelf --dyn-syms confirms full public API surface survives LTO
  • osx-aarch64 rebuilt and unaffected; ./mvnw -q -pl zstd,integration-tests -am test and ./mvnw -q validate (checkstyle) both pass
  • All six classifiers cross-compile cleanly
  • CI must pass on linux-x86_64/linux-aarch64 — this is the actual runtime-correctness check for this PR, not just a formality

🤖 Generated with Claude Code

dfa1 added a commit that referenced this pull request Jul 17, 2026
Adds .github/workflows/benchmark-lto.yml (workflow_dispatch only - JMH
runs are too slow for every push/PR, and this is throughput
exploration, not a correctness gate).

For each of linux-x86_64, linux-aarch64, osx-aarch64, windows-x86_64:
checks out main and an LTO variant branch (default
experiment/lto-linux, overridable via the lto_ref input) side by side,
builds+runs the CompressBenchmark/DecompressBenchmark suite (quick
smoke settings: 1 fork, 2 warmup, 5 measurement, size=65536) against
each, and writes a comparison table to the job summary via the new
.github/scripts/compare-benchmarks.py.

This turns the "-flto is Linux-only, no-op elsewhere" claim from #70/
#72 into CI evidence per platform instead of a manual assertion -
linux-x86_64/linux-aarch64 should show a real delta, osx-aarch64/
windows-x86_64 should show ~0% (LTO_FLAG stays empty for those
classifiers in scripts/build-zstd.sh).

Verified locally:
  - actionlint clean
  - compare-benchmarks.py tested against real JMH JSON output; found
    and fixed a real bug along the way - scoreError comes through as
    the JSON string "NaN" (not a number) when JMH can't compute a
    confidence interval, e.g. with few iterations
Add -flto for linux-* classifiers only. zig's Mach-O backend is a
self-hosted linker with no LTO support ("error: LTO requires using
LLD" even at the -c compile stage) — confirmed no workaround exists
locally (no real ld64.lld available, zig cc doesn't expose a linker
toggle). ELF is different: zig 0.16 still links it through the
bundled real lld by default, and -flto compiles and links cleanly
when cross-compiled to x86_64-linux-gnu/aarch64-linux-gnu from this
host, with the full public API surface (verified via
llvm-readelf --dyn-syms) intact after LTO.

What isn't verified locally: whether the LTO-linked library is
actually correct at runtime — this Mac can't execute Linux ELF
binaries. zstd/pom.xml and integration-tests/pom.xml activate their
native-classifier profile by host OS/arch, so ci.yml's ubuntu-latest
job will natively build and test-run exactly the linux-x86_64
classifier this touches. Marking this experimental pending that
signal, rather than folding it into #71 (asm/arch/hardening), which
only contains changes verified end-to-end on this host.

osx-*/windows-* classifiers are untouched (LTO_FLAG stays empty);
confirmed unaffected locally (rebuilt + zstd/integration-tests suite
passes on osx-aarch64, all six classifiers still cross-compile).

Ref #70
@dfa1
dfa1 force-pushed the experiment/lto-linux branch from e50d7ed to 459f6c7 Compare July 17, 2026 19:23
@dfa1

dfa1 commented Jul 17, 2026

Copy link
Copy Markdown
Owner Author

Benchmark results: LTO doesn't help, mildly hurts on x86_64

Ran the new benchmark-lto.yml workflow (main vs this branch) via runs 29597404273 and 29607345776 — the second after rebasing this branch onto current main (it previously predated #71/#73, which confounded the first run's numbers with unrelated ASM/arch/hardening changes).

linux-x86_64 (LTO applies) — real regression on our code path:

  • Compress: zstdJavaBytes -5.1%, zstdJavaSegment -4.5%
  • Decompress: zstdJavaBytes -0.4%, zstdJavaSegment -2.2%
  • Control check: zstdJni/aircompressor (unrelated to this branch's changes) stayed within ±0.5%, confirming the zstd-java deltas are real signal, not run-to-run noise.

linux-aarch64 (LTO applies) — a wash, all deltas within ±0.9%, controls equally flat.

osx-aarch64 (LTO is a no-op by design — LTO_FLAG stays empty, binary is identical with/without) — showed some swings, but the unrelated zstdJni/aircompressor benchmarks swung by similar amounts, which is environmental noise on that runner rather than signal.

Bottom line: LTO doesn't move the needle positively anywhere, and measurably hurts x86_64 compression throughput. This is a single quick JMH run (1 fork / 2 warmup / 5 measurement — directional per ADR 0012, not publication-grade), but the direction is consistent and the control comparisons rule out noise as the explanation on Linux.

Leaving this open rather than merging - the data doesn't support it as-is. Possible next steps if we want to revisit: a heavier/more-iterations run to confirm, or investigating why LTO regresses zstd's compress path here (e.g. it may be pessimizing branch layout or inlining decisions the hand-tuned zstd C code already relies on) before trying again.

@dfa1 dfa1 mentioned this pull request Jul 18, 2026
@dfa1

dfa1 commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Closing without merging.

A second measurement (main vs this branch, run 29634524084) replicates the original finding and makes it look worse, not better:

linux-x86_64 (LTO applies):

  • Compress: zstdJavaBytes -8.6%, zstdJavaSegment -8.7% (vs -5.1%/-4.5% in the first run)
  • Decompress: flat (+0.1% to +0.3%), no gain this time (vs +2.2%/-0.4% first run)
  • Controls (zstdJni/aircompressor) stayed within ±1.2%, confirming the compress regression is real signal, not noise.

linux-aarch64 (control): flat both times, ±0.8% max.

Two independent runs now agree LTO consistently regresses compress throughput on x86_64, with no reliable decompress upside to offset it. Combined with the macOS blocker (zig's Mach-O linker has no LTO support at all), this isn't worth pursuing further. Full writeup and first measurement on #70.

@dfa1 dfa1 closed this Jul 18, 2026
@dfa1
dfa1 deleted the experiment/lto-linux branch July 18, 2026 07:07
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