Repository navigation
Description
Now that Persimmon is on 0.13.1, I compared it with Kakadu 8.6.2 again (Persimmon's adapter built natively for macOS arm64, every 1024 px tile at every scale, JPEG output). On the 18000×12000 images j2k is now within 3–12% of Kakadu, at about the same memory, which is great. The remaining gap is on the 6000×4000 archival image. That's the kind of untiled, Kakadu-encoded JP2 our repository holds, so it's what Persimmon mostly serves. There Kakadu was 1.5–1.6 times faster.
Why. That image is classic, with 7 layers, 64×64 code-blocks, RPCL and Cmodes=BYPASS. On a serial 1024² region, Tier-1 is about 84% of the decode (66–68 ms of about 79 ms). A sample profile shows the whole of Tier-1 on the per-coefficient passes, arithmetic.rs and bypass.rs, arithmetic passes included. reset_for_job only uses the packed columns when every segment is arithmetic and the style is single_codeword_segment(), because packed and per-coefficient state can't be mixed within a block. So any selective bypass, or termination on each pass, keeps a block off the passes #303 sped up.
Prototype. I tried giving those blocks packed passes, on a branch off main:
- Eligibility.
packed_columns = !vertically_causal_context. Bypass, termination on each pass, context resets and segmentation symbols all take the packed columns. - Raw passes.
flag_passes.rsgainssignificance_propagation_pass_raw,magnitude_refinement_pass_rawandcleanup_pass_raw. They make the decisions of the packed arithmetic passes in the same order, reading raw bits: no contexts, and the raw sign bit is the sign, with no XOR. The arithmetic passes are untouched. - Raw reader. A
RawBitsreader buffers up to 64 de-stuffed bits. It skips the top bit of the byte after a 0xFF and returns ones once the segment is exhausted, asBypassDecoderdoes. Before a 0xFF whose stuffing it can't vouch for (the stuffed bit is one, or the data ends), it stops buffering and hands the rest of the segment to aBypassDecoder. So strict and lenient decodes of malformed data come out exactly as now. - Dispatch. Both segment loops in
schedule.rsrun raw segments on the packed raw passes when the block uses packed columns, andreset_for_next_bitplaneskips its per-coefficient bypass bookkeeping for packed blocks. - Tests. A test-only switch,
FORCE_PER_COEFFICIENT, keeps the per-coefficient passes testable, and the four state tests that check their internals set it. A new parity test encodes random code-blocks in all 16 non-causal style combinations, 8 sizes, 4 sub-band types and 1 to 16 bitplanes, about a quarter of them with raw segments. It decodes each with both sets of passes, strict and lenient, and also 24 corrupt copies of each (bit flips, bytes set to 0xFF, truncation). Samples and errors match exactly. With a deliberately flipped raw sign bit, it fails at once.
Results. Serial component-plane region decodes, against main, medians of 11 runs. Output hashes and peak heap are identical.
| Region | main | Prototype |
|---|---|---|
| archival 1024², middle | 87.2 ms | 74.6 ms (−14%) |
| archival 1024², top-left | 53.7 ms | 46.8 ms (−13%) |
| archival 1024² at ½ | 137.1 ms | 111.6 ms (−19%) |
| archival 512² | 27.4 ms | 22.9 ms (−16%) |
| 18000×12000 PLT 1024² (no bypass) | 118.0 ms | 119.7 ms |
| 18000×12000 no PLT 1024² (no bypass) | 141.4 ms | 140.4 ms |
In Persimmon's tile benchmark on the archival image, with Kakadu, 0.13.1 and the prototype run alternately:
| Kakadu 8.6.2 | j2k 0.13.1 | Prototype | |
|---|---|---|---|
| Concurrency 1, mean tile time | 52.7 ms | 80.0 ms | 68.9 ms |
| Concurrency 8, tiles/s | 124.5 | 77.3 | 94.2 |
That takes the gap from about 1.5× to about 1.3×. The gain is larger than the raw passes alone would explain, because moving the block onto packed columns speeds up its arithmetic passes too. After the change, the profile is about a third cleanup, a fifth arithmetic significance propagation and a sixth raw significance propagation, with the IDWT and output about 15%. The raw significance pass now costs about the same per decision as the arithmetic one: the cost is the column scanning and significance updates, not the bit reads.
Questions.
- This is right next to the code you just changed in perf(j2k-native): faster CPU classic Tier-1 decoding #303. Would you like a PR from me, or would you rather take it from here? I'm happy either way, and can push the branch for you to look at.
- Generalizing the arithmetic
Scanover a symbol source might avoid the duplicated raw passes, but I kept your arithmetic passes byte for byte so as not to disturb what perf(j2k-native): faster CPU classic Tier-1 decoding #303 tuned. Do you prefer one or the other? - Termination on each pass, context resets and segmentation symbols now take the packed columns too. The parity test covers all those combinations, but tell me if there's a reason they were kept off.
all_segments_arithmeticonreset_for_jobis now unused. I'd remove it in a PR.
Activity
jcwal1516 commentedon Oct 30, 2026
Awesome, thanks for finding this!
Since you offered either way, would you mind pushing your prototype and letting me take it from there? I have some CPU decode work that hasn't been pushed yet, including changes to
flag_passes.rsand the x86 dispatch inschedule.rs. From what you've described, that work sounds complementary to yours. It still keeps BYPASS blocks on the per-coefficient path, so this would address a gap it doesn't cover.Some numbers from the combined changes so far: on the M5 Ultra at six threads, a middle 1024² region of the 18000×12000 PLT image went from about 22 to 14.5 ms, and the HT image from 17.6 to 6.8 ms. Those are RGB8 region decodes, medians of five interleaved rounds against your #310 branch, with identical pixels. So there are some useful gains on top of your work already. I'd be interested to see what your BYPASS changes add to the combination, especially on the archival image.
With the overlap in those files, it probably makes sense for me to handle combining them. I'll preserve your commits where possible and credit your work and tests in the PR, then run the combination on ARM and x86 and share the results here.
Thanks again for finding this and offering to share the code!
jcoyne commentedon Oct 30, 2026
Sounds good, and those numbers are impressive. Please take it from here.
It's pushed as one commit on
jcoyne:perf/bypass-passes,4ecd3c42, on top of629dc030. Beyond what I described above, it also drops the now-unusedall_segments_arithmeticparameter fromreset_for_job, and it passes clippy, clippy-strict and the j2k-native tests. Two things I didn't get to:tests/classic_passes.rsis the coverage so far.Some context on how much this matters for us. Our repository's encoding settings changed recently: new images are encoded without
Cmodes=BYPASS(for Photoshop compatibility) and with-rate -. So BYPASS mostly matters for images already in the collection, which is most of what we serve today. On a real image with the new settings (5661×8287, 8 layers, 3.46 bpp, gg891fv3841_0002), j2k 0.13.1 is already level with Kakadu in Persimmon's tile benchmark: 124.6 against 124.5 ms for full-resolution tiles, 144.9 against 152.2 ms across all scales, and 38.0 against 36.5 tiles/s at concurrency 8, with about the same peak footprint. That image would make a good benchmark for this kind of content.jcwal1516 commentedon Nov 2, 2026
Thanks again for sharing the prototype and parity tests. I’ve integrated them into #314, now merged into
main, with your co-author credit preserved.In the isolated ARM64 comparison, the BYPASS re-encode of the 5661×8287 RGB image took 22% less time for a one-thread full decode. The 1024×1024 region tests took 19% less time at one thread and 12% less at six threads, with identical output. ARM64 and x86-64 checks, plus CUDA and Metal validation, passed.
We also ran the archival workload suite on both the Mac mini and Studio with your BYPASS implementation included, covering full images, reduced decodes and IIIF tile/region requests. The pixel comparisons were clean. We used the
gg891file you suggested and several other public JP2s from the same site:The original
gg891is non-BYPASS; its BYPASS control was generated locally. Those broader runs also included additional streaming improvements, so I’m keeping their combined results separate from the isolated BYPASS comparison. The follow-up series is now open in #316, #317 and #318.perf(j2k-native): decode bypass and terminated classic blocks on the …