Speed up lossless decoding - #11
Merged
Merged
Conversation
Times ours, libwebp, wasm, webp-rust and x/image decoding the same libwebp-encoded files, in a lossless and a lossy mode. Conversion to packed RGBA is inside the measurement, since x/image and gen2brain hand back YCbCr planes and libwebp does not. Captured on both machines: we edge out x/image on lossy and trail it by 1.4-3.9x on lossless.
Reads the decode tables out of results.md like the other figures do, on the same bar layout. The decode table has the peak-RSS table's column count, so the parser now takes the caption above a table as what tells them apart.
The lossless decoder read Huffman codes one bit at a time and resolved each prefix through a map keyed by the bits seen so far, which put map lookups at half of decode time. It now builds libwebp's two-level canonical lookup table and reads a symbol with one peek and one indexed load, with a second level only for codes longer than eight bits. Around that: - The bit reader keeps the stream in a buffer padded with zeros, so peeking eight bytes needs no end-of-stream branch and inlines into the symbol reader. Running past the end is caught once per pixel instead of once per symbol. - The inverse transforms run over the decoded buffer in place, halving the memory a decode holds. - The predictor transform picks its mode once per transform tile and runs a loop per mode, carrying the left and top-left neighbours in registers. - The pixel loop tracks x and y instead of dividing, and looks up the Huffman group only when the pixel crosses a tile. On an M4 Pro, decoding libwebp's lossless output of the test photos: 2.7-3.1x faster on the 5.5 MP images, and 0.78-1.08x golang.org/x/image/webp's time where it was 1.4-3.0x before. Adds a decode benchmark and match-against-x/image test to the benchmark module, and a corrupt-input test covering the bit reader's new bounds contract.
The loop reloaded the Huffman metadata and the colour cache through pointers on every pixel. Reading them into locals once turns a cache insert into a multiply, a shift and a store.
The VP8L rewrite landed after the last decode capture, so both tables still showed the old numbers. Lossless decode now runs level with x/image and ahead of the Rust original. Both machines now build webp-rust from the v0.2.0 tag; colossus had been carrying an untagged 0.2.1 tree whose lossless decoder differs.
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.
Speeds up VP8L (lossless) decoding, adds a decode pass to the benchmark suite,
and recaptures the decode numbers on both machines.
Where the time was going
The VP8L decoder read Huffman codes one bit at a time and resolved each prefix
through a
map[uint16]uint16per code length. Map lookups alone were ~47% ofdecode time.
What changed
root table, with second-level tables only for codes longer than 8 bits. A
symbol is one peek and one indexed load.
peeking 8 bytes needs no end-of-stream check and inlines into the symbol
reader. Overrun is caught once per pixel instead of once per symbol.
read pixels only after their predictors are reconstructed, so they run over the
decoded buffer. Peak decode memory dropped roughly by half.
chosen once per span and each mode is its own loop, with left and top-left
neighbours carried in registers.
group only on tile crossings, and keeps metadata and color cache in locals.
Three things measured slower on the M4 and were reverted: SWAR sum-of-absolute-
differences for the Select predictor, representing single-symbol trees as a
filled root table, and root tables of 7, 9 or 10 bits.
Results
Lossless decode,
oursagainst the field, across the seven test images:On the geometric mean we are ahead of
x/imagein every mode on both machines,by 1% to 18%. Before this branch, lossless decode ran 1.4-3.9x slower than
x/imageand level with the Rust original.Lossy decode is untouched: 0.86-0.99x
x/image, 2.4-5.8x libwebp.Benchmark harness
benchmark/run-decode.shand the decode path inwebpbench/rustbench, whichadd
golang.org/x/image/webpas a fifth engine. Every engine decodes the samelibwebp-encoded file and ends at packed RGBA, so the YCbCr-returning engines
pay for that conversion inside the measurement.
benchmark/chart, on the same bar layout as theexisting figures.
webp-rustfrom the v0.2.0 tag. The amd64 host hadbeen carrying an untagged 0.2.1 tree whose lossless decoder differs, which is
why
benchmark/rustbench/Cargo.lockmoves.