Skip to content

perf: speed up htj2k encode 8-15% and small-chunk decode 6% - #34

Draft
nmoinvaz wants to merge 6 commits into
fideus-labs:mainfrom
nmoinvaz:perf/codestream-small-planes
Draft

nmoinvaz wants to merge 6 commits into
fideus-labs:mainfrom
nmoinvaz:perf/codestream-small-planes

Conversation

@nmoinvaz

Copy link
Copy Markdown
Contributor

Six follow-ups to #32, found by profiling the HTJ2K lanes against the perf guide. Codestreams and chunks stay byte-identical to main on every fixture checked: camera, label-map, mask, and sparse 1024² planes, 777×333, 16×32768, and 40000×3 planes, and a 32×64×64 chunk at 3 and 5 levels.

Block coder. Quad::load read each sample through its own checked read that skipped columns past the row's end. A missing sample codes exactly as a zero, so each row's two columns now load with one check and all four lanes compute without branches. It and the MagSgn writer's flush are now forced inline, which keeps the quad and the writer state in registers.

Wavelet. The horizontal 5/3 analysis read every other sample through lane inserts, and both directions left most of a short row to the scalar remainder of a 16-wide loop. Rows now split into their bands first, and bands under 128 samples lift through the 4-wide row kernels. A 64×64 plane's forward DWT is 7% faster and its inverse 13%.

Codestream. The writer writes packet headers first, which fixes the tile-part length and PLT, then copies block data once, straight into the output, instead of through per-packet, body, and tile-part buffers. The reader borrows a single tile-part's body instead of copying it, and the zarr decode range check folds in 32-bit lanes.

Single-threaded, Apple M5 main this PR change
1024² image plane encode 6.34 ms 5.37 ms -15%
1024² sparse plane encode 6.50 ms 5.74 ms -12%
1024² mask plane encode 6.65 ms 6.03 ms -9%
1024² label plane encode 6.21 ms 5.71 ms -8%
32×64×64 chunk encode 1.01 ms 0.89 ms -13%
32×64×64 chunk decode 0.77 ms 0.73 ms -6%
1024² image plane decode 4.22 ms 4.16 ms -1.5%
bytes allocated to encode the image plane 18.3 MB 11.7 MB -36%

The machine was loaded while measuring, so times are the fastest of nine interleaved runs. Instruction counts, which load does not move, fell 9.4% for plane encode and 10.2% for chunk encode.

Tried and dropped: storing small tag trees inline cut chunk-decode allocations by 42% with no measurable change in time, and forcing the VLC flush or emit_magsgn inline cost 2-7%.

The horizontal 5/3 analysis read every other sample through lane
inserts, and both directions left most of a short row to the scalar
remainder of loops LLVM vectorizes 16 samples at a time. Each row now
splits into its bands once, and bands under 128 samples lift through
the lane's row kernels. Output is bit-identical. A 64x64 plane's
forward DWT is 7% faster and its inverse 13%.

Assisted-By: Claude <noreply@anthropic.com>
The writer staged each packet in its own buffer, appended it to a body,
copied the body into a tile-part, and copied that into the output.
Packet headers are now written first, which fixes the tile-part length
and PLT, so block data is copied once, into the output. Output is
byte-identical. Plane encode retires 1% fewer instructions, and peak
memory holds two copies of the coded data instead of three.

Assisted-By: Claude <noreply@anthropic.com>
`narrow_samples` widened every decoded sample to i64 before folding
its minimum and maximum, so the reduction ran two lanes per vector. The
bounds of i32 samples are themselves i32, so the fold now runs in i32
and widens once. Chunk decode takes 4% fewer cycles.

Assisted-By: Claude <noreply@anthropic.com>
Decoding copied every tile-part's packet body into a fresh buffer before
parsing a single packet, a pass over the whole compressed plane. A
single tile-part, which is all the writer emits, is already contiguous
in the codestream, so its body is now borrowed. Plane decode takes 3.5%
fewer cycles.

Assisted-By: Claude <noreply@anthropic.com>
`Quad::load` read its four samples through separate checked reads that
each skipped a sample past the row's end. A missing sample codes
exactly as a zero, so away from a row's end both columns of each row
now load with one check and every lane computes the same way. The load
is forced inline, since its larger body otherwise stays out of line and
returns the quad through memory. Output is byte-identical. Plane encode
retires 4% fewer instructions and a 64x64 chunk encodes 6% faster.

Assisted-By: Claude <noreply@anthropic.com>
The flush that writes out a quad's MagSgn bytes stayed out of line, so
the accumulator and stream length round-tripped through memory at
every quad. It is now forced inline, with the stuffing path still out
of line. Output is byte-identical. Plane encode retires 3.5% fewer
instructions and runs 5% faster, and a sparse plane 3%.

Assisted-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 29, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@nmoinvaz

Copy link
Copy Markdown
Contributor Author

Claude is telling me regarding the failed CI:

Every flagged lane runs code this PR doesn't touch, and none of them reproduce locally.

This branch has not been deployed

No deployments
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.

1 participant