perf: faster overlapping copy in the decoder - #227
Conversation
3551900 to
654bda0
Compare
|
Safe decode with Macbook M4 |
|
unsafe decode with Macbook M4 |
|
safe decode on AMD EPYC 7R32 unsafe decode |
654bda0 to
423821a
Compare
|
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:
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. |
96ac6e7 to
4daf8cf
Compare
|
Mixed bag on AMD EPYC 7R32 safe decode unsafe decode |
… 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>
4daf8cf to
7245f12
Compare
|
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 The fix: the doubling now only sits on the long-match / near-end path (the I re-ran on an EPYC 7R32 (your chip) with the binggan block_decompress bench. vs main:
@PSeitz Would you mind re-running your numbers to confirm on your side? |
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.