Skip to content

perf: faster overlapping copy in the decoder - #227

Open
richarddd wants to merge 1 commit into
PSeitz:mainfrom
richarddd:faster-overlapping-copy
Open

perf: faster overlapping copy in the decoder#227
richarddd wants to merge 1 commit into
PSeitz:mainfrom
richarddd:faster-overlapping-copy

Conversation

@richarddd

@richarddd richarddd commented Jun 19, 2026

Copy link
Copy Markdown

Long self-referential (overlapping / run) match copies in the unsafe decoder now grow by doubling the valid prefix with memcpy instead of copying byte-by-byte (10-40x on run/periodic data), spliced into the long-match path only and passed by value so the hot fast loop's codegen is unchanged and general-data decode stays flat on both x86 (Zen 2) and ARM. Validated under miri and against the C decoder.

@richarddd
richarddd force-pushed the faster-overlapping-copy branch from 3551900 to 654bda0 Compare June 20, 2026 20:36
@PSeitz

PSeitz commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Safe decode with Macbook M4

block_decompress
725
lz4 flex    Memory: 725 B     Avg: 5.3309 GB/s (-3.68%)    Median: 5.3309 GB/s (-2.94%)    [5.1786 GB/s .. 5.4104 GB/s]    Output: 725
34308
lz4 flex    Memory: 34.3 KB     Avg: 3.5030 GB/s (-2.29%)    Median: 3.4994 GB/s (-2.22%)    [3.4774 GB/s .. 3.5523 GB/s]    Output: 34_308
64723
lz4 flex    Memory: 64.7 KB     Avg: 2.9131 GB/s (-1.53%)    Median: 2.9282 GB/s (-1.46%)    [2.7555 GB/s .. 2.9638 GB/s]    Output: 64_723
66675
lz4 flex    Memory: 66.7 KB     Avg: 5.6266 GB/s (-7.68%)    Median: 5.6370 GB/s (-7.82%)    [5.2850 GB/s .. 5.8059 GB/s]    Output: 66_675
9991663
lz4 flex    Memory: 10.0 MB     Avg: 3.4665 GB/s (-3.09%)    Median: 3.4710 GB/s (-3.30%)    [3.3536 GB/s .. 3.5056 GB/s]    Output: 9_991_663
96274
lz4 flex    Memory: 96.3 KB     Avg: 13.965 GB/s (+4.24%)    Median: 13.961 GB/s (+3.64%)    [13.596 GB/s .. 14.462 GB/s]    Output: 96_274

@PSeitz

PSeitz commented Jun 21, 2026

Copy link
Copy Markdown
Owner

unsafe decode with Macbook M4

block_decompress
725
lz4 flex    Memory: 725 B     Avg: 6.4732 GB/s (-5.36%)    Median: 6.4732 GB/s (-5.36%)    [6.1966 GB/s .. 6.5909 GB/s]    Output: 725
34308
lz4 flex    Memory: 34.3 KB     Avg: 3.9557 GB/s (-1.14%)    Median: 3.9649 GB/s (-0.79%)    [3.8705 GB/s .. 4.0070 GB/s]    Output: 34_308
64723
lz4 flex    Memory: 64.7 KB     Avg: 4.1465 GB/s (+0.27%)    Median: 4.1417 GB/s (+0.47%)    [4.1183 GB/s .. 4.1870 GB/s]    Output: 64_723
66675
lz4 flex    Memory: 66.7 KB     Avg: 12.498 GB/s (+6.37%)    Median: 12.488 GB/s (+6.44%)    [12.281 GB/s .. 12.671 GB/s]    Output: 66_675
9991663
lz4 flex    Memory: 10.0 MB     Avg: 3.7407 GB/s (+0.76%)    Median: 3.7532 GB/s (-1.18%)    [3.5468 GB/s .. 3.8162 GB/s]    Output: 9_991_663
96274
lz4 flex    Memory: 96.3 KB     Avg: 46.803 GB/s (-3.55%)    Median: 46.849 GB/s (-2.68%)    [45.541 GB/s .. 48.403 GB/s]    Output: 96_274

@PSeitz

PSeitz commented Jun 21, 2026

Copy link
Copy Markdown
Owner

safe decode on AMD EPYC 7R32

block_decompress
725
lz4 flex    Memory: 725 B     Avg: 2.0365 GB/s (-17.13%)    Median: 2.0195 GB/s (-17.83%)    [1.9595 GB/s .. 2.1642 GB/s]    Output: 725
34308
lz4 flex    Memory: 34.3 KB     Avg: 733.37 MB/s (-13.76%)    Median: 730.55 MB/s (-14.21%)    [722.33 MB/s .. 751.10 MB/s]    Output: 34_308
64723
lz4 flex    Memory: 64.7 KB     Avg: 779.32 MB/s (-13.88%)    Median: 780.50 MB/s (-13.96%)    [763.18 MB/s .. 784.58 MB/s]    Output: 64_723
66675
lz4 flex    Memory: 66.7 KB     Avg: 1.2015 GB/s (-4.73%)    Median: 1.2014 GB/s (-4.83%)    [1.1864 GB/s .. 1.2085 GB/s]    Output: 66_675
9991663
lz4 flex    Memory: 10.0 MB     Avg: 881.23 MB/s (-14.75%)    Median: 882.32 MB/s (-14.67%)    [861.00 MB/s .. 888.72 MB/s]    Output: 9_991_663
96274
lz4 flex    Memory: 96.3 KB     Avg: 1.8316 GB/s (+0.55%)    Median: 1.8308 GB/s (-0.14%)    [1.8245 GB/s .. 1.8376 GB/s]    Output: 96_274

unsafe decode

block_decompress
725
lz4 flex    Memory: 725 B     Avg: 3.1385 GB/s (-12.99%)    Median: 3.1385 GB/s (-12.55%)    [3.0851 GB/s .. 3.1798 GB/s]    Output: 725
34308
lz4 flex    Memory: 34.3 KB     Avg: 2.0681 GB/s (-9.75%)    Median: 2.0682 GB/s (-10.13%)    [2.0639 GB/s .. 2.0726 GB/s]    Output: 34_308
64723
lz4 flex    Memory: 64.7 KB     Avg: 2.1462 GB/s (-7.41%)    Median: 2.1461 GB/s (-7.63%)    [2.1402 GB/s .. 2.1506 GB/s]    Output: 64_723
66675
lz4 flex    Memory: 66.7 KB     Avg: 4.8280 GB/s (+1.48%)    Median: 4.8245 GB/s (+0.97%)    [4.6548 GB/s .. 4.9910 GB/s]    Output: 66_675
9991663
lz4 flex    Memory: 10.0 MB     Avg: 1.0897 GB/s (-4.19%)    Median: 1.1021 GB/s (-3.22%)    [920.28 MB/s .. 1.1052 GB/s]    Output: 9_991_663
96274
lz4 flex    Memory: 96.3 KB     Avg: 21.980 GB/s (-16.12%)    Median: 22.224 GB/s (-15.14%)    [21.220 GB/s .. 22.291 GB/s]    Output: 96_274

@richarddd
richarddd force-pushed the faster-overlapping-copy branch from 654bda0 to 423821a Compare June 21, 2026 22:51
@richarddd

Copy link
Copy Markdown
Author

Good catch, you were right.

The slowdown was the new doubling copy being slower than the plain byte loop for short overlapping matches, and that's the common case for normal data. The main decode loop uses that path for every match up to 18 bytes, so swapping the tight byte loop for variable-size memcpys there cost a few percent everywhere.

Fixed it so short copies keep the byte loop and only long copies (periodic/run data) use the doubling, with the cutoff at length 33. So normal data goes back to the old fast path (actually a touch faster now, since near-end copies do a single memcpy instead of looping), and periodic/run data keeps the big speedup.

Numbers on my M4, vs main, both decoders:

  • general (34k/65k/json/10mb/logo): was up to ~15% slower, now roughly +4 to +12%
  • periodic/runs: still 15-40x faster

I don't have an EPYC to test on though. Could you re-run your numbers there? Should be sorted since it was the same root cause, but would be good to confirm.

@richarddd
richarddd force-pushed the faster-overlapping-copy branch 2 times, most recently from 96ac6e7 to 4daf8cf Compare June 22, 2026 05:20
@PSeitz

PSeitz commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Mixed bag on AMD EPYC 7R32

safe decode

block_decompress
725
lz4 flex    Memory: 725 B     Avg: 2.5087 GB/s (+1.73%)    Median: 2.5087 GB/s (+2.08%)    [2.4744 GB/s .. 2.5439 GB/s]    Output: 725
34308
lz4 flex    Memory: 34.3 KB     Avg: 869.26 MB/s (+1.85%)    Median: 871.80 MB/s (+2.15%)    [848.77 MB/s .. 875.85 MB/s]    Output: 34_308
64723
lz4 flex    Memory: 64.7 KB     Avg: 926.22 MB/s (+1.54%)    Median: 928.86 MB/s (+1.84%)    [906.87 MB/s .. 931.19 MB/s]    Output: 64_723
66675
lz4 flex    Memory: 66.7 KB     Avg: 1.2302 GB/s (-3.54%)    Median: 1.2325 GB/s (-3.36%)    [1.1994 GB/s .. 1.2358 GB/s]    Output: 66_675
9991663
lz4 flex    Memory: 10.0 MB     Avg: 1.0412 GB/s (+1.12%)    Median: 1.0412 GB/s (+1.01%)    [1.0337 GB/s .. 1.0472 GB/s]    Output: 9_991_663
96274
lz4 flex    Memory: 96.3 KB     Avg: 1.8191 GB/s (-0.19%)    Median: 1.8217 GB/s (-0.05%)    [1.7277 GB/s .. 1.8283 GB/s]    Output: 96_274

unsafe decode

block_decompress
725
lz4 flex    Memory: 725 B     Avg: 3.5539 GB/s (-1.47%)    Median: 3.5539 GB/s (-1.47%)    [3.5194 GB/s .. 3.5714 GB/s]    Output: 725
34308
lz4 flex    Memory: 34.3 KB     Avg: 2.2326 GB/s (-1.25%)    Median: 2.2330 GB/s (-2.79%)    [2.2262 GB/s .. 2.2352 GB/s]    Output: 34_308
64723
lz4 flex    Memory: 64.7 KB     Avg: 2.3154 GB/s (-0.90%)    Median: 2.3161 GB/s (-0.62%)    [2.2882 GB/s .. 2.3202 GB/s]    Output: 64_723
66675
lz4 flex    Memory: 66.7 KB     Avg: 5.3861 GB/s (+3.09%)    Median: 5.4119 GB/s (+3.05%)    [5.0986 GB/s .. 5.4567 GB/s]    Output: 66_675
9991663
lz4 flex    Memory: 10.0 MB     Avg: 1.1305 GB/s (-0.99%)    Median: 1.1303 GB/s (-1.00%)    [1.1251 GB/s .. 1.1363 GB/s]    Output: 9_991_663
96274
lz4 flex    Memory: 96.3 KB     Avg: 22.255 GB/s (-11.23%)    Median: 22.265 GB/s (-11.19%)    [21.920 GB/s .. 22.322 GB/s]    Output: 96_274

… run data)

Self-referential (offset < match_length) match copies were done byte-by-byte. Long
ones (run/periodic data) now grow by doubling the valid prefix with memcpy: O(log n)
copies instead of n, ~10-40x faster. This is spliced into the `duplicate`
general-match path only, so the hot <=18 fast loop is untouched and general-data
decode is unchanged (measured neutral to +4% on AMD EPYC 7R32 / Zen 2 and Apple M4).

The doubling helper is cold/inline(never) and takes the output pointer by value
(returning the advance) rather than by `&mut`. Passing a `&mut` to a non-inlined
function forces the pointer onto the stack across the whole decode loop, which on
its own cost ~10% on general data on Zen 2.

Unsafe decoder only. The safe decoder is left as-is: it is more codegen-sensitive
and regressed when touched, and its offset==1 runs already use a fast fill.

Adds tests/overlap_miri.rs and a CI miri job for the unsafe pointer code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@richarddd
richarddd force-pushed the faster-overlapping-copy branch from 4daf8cf to 7245f12 Compare June 23, 2026 12:40
@richarddd

richarddd commented Jun 23, 2026

Copy link
Copy Markdown
Author

Tracked it down, and it was real. The slowdown wasn't the algorithm, it was codegen: the doubling code was landing in the hot decode loop, and in one variant a &mut pointer argument forced the output pointer onto the stack across the whole loop. That decode loop is tiny and Zen 2 is very sensitive to it, so even a handful of extra instructions cost ~5-12% on general data.

The fix: the doubling now only sits on the long-match / near-end path (the <=18 fast loop is byte-for-byte identical to before), it's a separate cold out-of-line function, and it takes the output pointer by value instead of &mut so nothing spills. Unsafe decoder only. I left the safe decoder alone, since it's even more codegen-sensitive and its offset==1 runs already use a fast fill.

I re-ran on an EPYC 7R32 (your chip) with the binggan block_decompress bench. vs main:

  • 725 / 34k / 65k / json / 10mb / logo: all within ~1% (json and 10mb a touch faster)
  • run/periodic data: 10-40x faster

@PSeitz Would you mind re-running your numbers to confirm on your side?

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