perf(decode): prefetch the decode index eight tokens ahead - #2388
Closed
ArthurZucker wants to merge 1 commit into
Closed
ArthurZucker wants to merge 1 commit into
ArthurZucker wants to merge 1 commit into
Conversation
Marginal but real, and measured: english +3.1%, japanese flat.
After the id-ordered index, decode's whole remaining cost is one random access
per token into `decode_off`, which is indexed by a token id and so has no
spatial locality to exploit. A decoder already holds every id it is about to
need, so the load can be issued ahead of use: `prfm pldl1keep` on aarch64,
`_mm_prefetch` on x86_64, nothing anywhere else. It is a hint -- it cannot
fault, an out-of-range id is filtered before it, and the portable build simply
does not emit it.
Eight ahead covers an L2 hit at this loop's ~9 ns/token without running so far
ahead that the line is evicted before it is used.
Why only 3% when the table being warmed accounts for far more than that: the
loop's iterations were already independent, so the out-of-order engine was
overlapping these loads on its own. Decoding one chunk repeatedly, so the
tables collapse into L1, runs at 4.08 ns/token against 9.08 -- but that 2.2x is
a cache *capacity* effect across the whole working set, not per-access latency
sitting idle waiting to be hidden. Prefetching cannot shrink a working set.
Two things measured and rejected on the way here, recorded so they are not
retried:
* Inlining short tokens into the index (8-byte entry, tag byte plus seven
payload bytes, spilling longer tokens to the slab). Flat on both corpora,
and it is NOT because the path was cold: 88.5% of english and 95.0% of
japanese decoded token instances are 7 bytes or shorter. It removes the
second dependent load, but that load was already cheap -- the slab is
id-ordered, so it tracks text locality -- while the entry grew from 4 to 8
bytes and doubled the footprint of the one table that does miss. English
touches 31,395 distinct ids, so live index went ~126 kB -> ~251 kB. The
index wants to be small, not informative.
* SIMD UTF-8 validation. `from_utf8` is 1.1% of english decode (0.02 ns/byte
-- std's ASCII fast path) and 27.0% of japanese (0.54 ns/byte). Worth
revisiting for non-Latin only, and worth nothing on Latin.
Collaborator
Author
|
Closing: measured +3.1% on english and flat on japanese, which is not worth arch-gated inline asm in a hot path. The analysis stands in the commit message and in #2387 — the remaining headroom is cache capacity on a table sized by the id space, not latency that a prefetch can hide. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #2387. Marginal — english +3.1%, japanese flat. Opened so the measurement exists as an artifact; close it if 3% is not worth ~35 lines of arch-gated inline asm in a hot path.
What it does
After #2387's id-ordered index, decode's entire remaining cost is one random access per token into
decode_off, indexed by a token id and therefore with no spatial locality to exploit. A decoder already holds every id it is about to need, so the load can be issued ahead of use:prfm pldl1keepon aarch64,_mm_prefetchon x86_64, nothing elsewhere. A hint only — it cannot fault, an out-of-range id is filtered before it, and the portable build emits nothing.Eight ahead covers an L2 hit at ~9 ns/token without running so far ahead that the line is evicted before use.
Why only 3%
The iterations were already independent, so the out-of-order engine was overlapping these loads without help. Decoding one chunk repeatedly, so the tables collapse into L1, runs the identical loop at 4.08 ns/token against 9.08 — but that 2.2× is a cache capacity effect across the whole working set, not per-access latency sitting idle waiting to be hidden. Prefetching cannot shrink a working set.
That is also the honest ceiling statement for this loop: the remaining headroom is the size of a table indexed by the id space, and ids cannot be renumbered — they are the model contract.
Rejected on the way, with numbers
Inlining short tokens into the index (8-byte entry: tag byte + 7 payload bytes, longer tokens spilling to the slab). Flat on both corpora — and not because the path was cold:
It does remove the second dependent load, but that load was already cheap — the slab is id-ordered, so it tracks text locality — while the entry grew 4 → 8 bytes and doubled the footprint of the one table that does miss. English touches 31,395 distinct ids, so the live index went ~126 kB → ~251 kB. The index wants to be small, not informative.
SIMD UTF-8 validation. Measured share of decode time:
from_utf8std's ASCII fast path already runs at 0.02 ns/byte, so a SIMD validator is worth nothing on Latin text. It is worth revisiting for non-Latin, where a 4× validator would buy roughly +25% on japanese.
Tests
cargo test -p tk-encodegreen (176 + 2). clippy andfmt --checkclean. No behaviour change: the prefetch is a hint, and the sparse-id equivalence test from #2387 still gates the lookup itself.