Skip to content

[fast] Keep only ip0 and ip2 in the noDict search loop - #4865

Open
jeffchen006 wants to merge 1 commit into
facebook:devfrom
jeffchen006:fast-nodict-fewer-pointers
Open

jeffchen006 wants to merge 1 commit into
facebook:devfrom
jeffchen006:fast-nodict-fewer-pointers

Conversation

@jeffchen006

@jeffchen006 jeffchen006 commented Oct 26, 2026 •

Copy link
Copy Markdown

In ZSTD_compressBlock_fast_noDict_generic(), ip1 is always ip0 + 1 and ip3 is always ip2 + 1. This PR removes those two variables and computes them from ip0 and ip2 where they are used.

Every pointer value, hash table write, prefetch address and loop condition stays the same, so the compressed output is byte-identical. The only difference is that the hot loop has two fewer live pointers. With clang 14, ZSTD_compressBlock_fast() has 102 fewer stack accesses (static count, 884 → 782) and runs 1.6% fewer instructions for -1 on silesia.tar. With gcc 11 the static count changes less (684 → 671) and the gain is smaller.

The change applies to both useCmov variants (#4165), because they share the generic function.

Results

silesia.tar, whole file, compression, zstd -b<level> -i2. Each cell is the median of 10 alternating dev/PR pairs on one pinned core. "Change" is the median of the per-pair ratios.

Compiler Level dev MB/s PR MB/s Change Faster pairs
clang 14.0.0 1 479.5 499.4 +4.09% 10/10
clang 14.0.0 2 406.5 416.2 +2.54% 10/10
clang 14.0.0 --fast=1 532.0 549.4 +3.40% 10/10
clang 14.0.0 --fast=3 603.6 623.1 +2.99% 10/10
clang 14.0.0 --fast=5 666.1 682.7 +2.48% 10/10
clang 14.0.0 --fast=10 804.5 820.1 +1.93% 10/10
gcc 11.4.0 1 492.8 495.9 +0.66% 10/10
gcc 11.4.0 2 411.9 415.8 +0.83% 10/10
gcc 11.4.0 --fast=1 543.2 552.8 +1.74% 10/10
gcc 11.4.0 --fast=3 615.4 624.3 +1.46% 10/10
gcc 11.4.0 --fast=5 683.3 688.2 +0.57% 9/10
gcc 11.4.0 --fast=10 828.2 833.3 +0.80% 10/10

The compressed sizes are identical to dev in every cell (for example, 73,191,101 bytes at -1 and 69,303,779 bytes at -2). A baseline-vs-baseline (A/A) run on the same core gave −0.03% (gcc) and −0.11% (clang) at -1.

Unchanged paths (the code is not used, or the effect is noise):

Compiler Case Change Faster pairs
gcc 11.4.0 level 3 (dfast) −0.00% 6/12
clang 14.0.0 level 3 (dfast) −0.12% 6/12
gcc 11.4.0 level 1 / 2 decompression −0.17% / +0.04% 4/10, 6/10
clang 14.0.0 level 1 / 2 decompression +0.13% / +0.12% 8/10, 6/10

Small blocks (-B, so silesia.tar is compressed as independent chunks, which selects the cmov variant), level 1:

Compiler Block dev MB/s PR MB/s Change Faster pairs
clang 14.0.0 4 KB 295.6 297.1 +0.63% 10/10
clang 14.0.0 16 KB 370.9 373.1 +0.66% 9/10
gcc 11.4.0 4 KB 309.2 308.8 −0.09% 5/10
gcc 11.4.0 16 KB 387.4 386.7 −0.25% 1/10

The gcc 16 KB row is inside the A/A noise, but its sign is consistent, so I list it as neutral to slightly negative. With small chunks, the per-block setup takes most of the time and the loop runs only a few iterations.

Code size

size text, zstd CLI built with make zstd:

Compiler dev PR Delta
gcc 11.4.0 1,042,034 1,041,810 −224
clang 14.0.0 967,602 966,514 −1,088

zstd_fast.o: gcc −256 bytes, clang −1,072 bytes.

Correctness

  • Equivalence: at the loop head, ip1 == ip0 + 1 and ip3 == ip2 + 1 always hold. They hold at _start, and after each iteration the old ip3 = ip1 + step = ip0 + 1 + step = ip2 + 1. Every use of the removed pointers maps one-to-one to ip0 + 1 or ip2 + 1. _offset, _match and _cleanup do not read ip1 or ip3.
  • Byte identity, with cross-decoding between dev and PR: about 70,000 cases had 0 differences. They cover --fast=1..10, -1, -2, minMatch 4–7, wlog 10/11/12/18, tlen, --long, -T2 --rsyncable, file and stdin input, and input sizes 0–80 B, 96–4097 B and 128 KB ± 10 B / 256 KB ± 10 B (block end). These builds were tested: gcc, clang, -m32, ZSTD_NO_INTRINSICS and DEBUGLEVEL=1.
  • Tests: fuzzer -T180s, zstreamtest -T180s, an ASan+UBSan fuzzer -T120s and playTests.sh all passed.
Method and more data
  • Machine: AMD EPYC 9654 (Zen 4), Ubuntu 22.04, Linux 5.15. The machine is shared, so each benchmark ran pinned to one core.
  • Builds: make zstd with default flags (-O3) and no -march.
  • Input: silesia.tar, 211,957,760 bytes, SHA-256 0b5ec70c8048321525f820bc1d76429840bd22ee2c7a116175253a0fec2c724d.
  • Command: zstd -b<level> -i2 (plus -B4K / -B16K for the small-block rows). Each pair runs dev and the PR back to back, and the order alternates between pairs.
  • Second independent run (12 pairs, different core): clang -1 +4.09% (12/12), clang -2 +2.02% (12/12), gcc -1 +0.62% (12/12), gcc -2 +1.05% (11/12).
  • Other parameter sets, whole silesia.tar, compression:
Compiler Parameters Change Faster pairs
clang 14.0.0 -1 --zstd=minMatch=4 +4.11% 9/10
clang 14.0.0 -1 --zstd=minMatch=5 +3.51% 10/10
clang 14.0.0 -1 --zstd=minMatch=6 +3.36% 10/10
clang 14.0.0 -1 --zstd=minMatch=7 +3.92% 10/10
clang 14.0.0 -1 --zstd=wlog=18 +2.55% 10/10
gcc 11.4.0 -1 --zstd=minMatch=4 +1.20% 10/10
gcc 11.4.0 -1 --zstd=minMatch=5 +1.29% 10/10
gcc 11.4.0 -1 --zstd=minMatch=6 +1.41% 10/10
gcc 11.4.0 -1 --zstd=minMatch=7 +0.71% 9/10
gcc 11.4.0 -1 --zstd=wlog=18 +0.62% 8/10
  • Builds with -O2, whole silesia.tar, compression: clang -1 +2.25%, -2 +1.85%; gcc -1 +0.82%, -2 +1.48%.
  • Per file (1 MB prefix of each Silesia file, -1, 8 pairs): clang +1.8% to +12.7% on 11 of 12 files, nci −0.09% (4/8). gcc +0.7% to +2.1% on all 12 files.
  • perf stat, zstd -1 --single-thread on the whole silesia.tar, 3 alternating runs:
    • clang: 6.069 G → 5.975 G instructions (−1.6%).
    • gcc: 5.997 G → 5.982 G instructions (−0.24%).
    • Branch misses unchanged.

Limits

  • Platforms: measured only on x86-64 (Zen 4) with gcc 11.4 and clang 14.0. I have no aarch64 or Apple M1 results and no newer-compiler results. The gain comes from register allocation, so it can be different with other compilers.
  • Scope: the change affects only the noDict fast loop (levels 1–2, negative levels, and strategy=fast). The dictMatchState and extDict variants are unchanged.

Checklist

  • I searched open pull requests and issues: this problem is not already being addressed.
  • Bug fix: it comes with a reproducer or a test that fails without the fix. (Not applicable: this is a performance change with identical output.)
  • Performance change: numbers from repeated runs are included, with CPU, compiler, flags and data.
    A gain in one configuration is not a general gain: see the Performance section of CONTRIBUTING.md.
  • I stated below whether AI or other automated tools were used.

Tools used

Claude Code (Claude Opus) found this optimization, wrote the patch, ran the correctness checks (byte-identity harnesses, fuzzer, zstreamtest, ASan/UBSan, playTests) and the benchmarks, and drafted this description. A separate Claude Code agent reviewed the patch independently and repeated the measurements. I reviewed the diff and the benchmark data before submitting.

In ZSTD_compressBlock_fast_noDict_generic(), ip1 is always ip0 + 1 and
ip3 is always ip2 + 1. Remove these two variables and derive them from
ip0 and ip2 where they are used. Every pointer value, hash table write,
prefetch address and loop condition stays the same, so the compressed
output is unchanged.

With two fewer live pointers in the hot loop, clang 14 generates 102
fewer stack accesses in ZSTD_compressBlock_fast() and executes 1.6%
fewer instructions for level 1 on silesia.tar.

Whole silesia.tar compression speed, AMD EPYC 9654, median of 10
alternating pairs:
  clang 14: level 1 +4.1%, level 2 +2.5%, --fast=1..10 +1.9% to +3.4%
  gcc 11:   level 1 +0.7%, level 2 +0.8%, --fast=1..10 +0.6% to +1.7%
Decompression and level 3 are neutral. Code size is smaller
(text: gcc -224 bytes, clang -1088 bytes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed label Oct 26, 2026
@jeffchen006
jeffchen006 marked this pull request as ready for review October 27, 2026 00:00

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant