Skip to content

perf(decode): single-band output renders straight into a zeroed image (system heap win vs zune) - #18

Merged
Ttimmahlax merged 1 commit into
mainfrom
perf/single-band-direct
Oct 23, 2026
Merged

Ttimmahlax merged 1 commit into
mainfrom
perf/single-band-direct

Conversation

@Ttimmahlax

Copy link
Copy Markdown
Contributor

After 0.4.5, ArtCraft still measured the default decoder behind zune-jpeg on one core under the system allocator: 1.050x on the photo and 1.009x on the frame. With a single output band, this PR renders rows directly into a zeroed image, which the Windows heap serves from already-zero pages. That drops the per-row copy into uninitialised memory. Multi-band output keeps the uninitialised path.

One core, default decode(), vs zune-jpeg 0.5.15:

system allocator rusty_alloc
frame1080 0.952 (18/20) 0.974 (19/20)
photo2000s 0.917 (20/20) 0.933 (20/20)
photo2000 0.912 (18/20) 0.906 (20/20)

The cost: about 2% vs 0.4.5 under rusty_alloc, where zeroing recycled memory costs a little more than the copy did. It still beats zune there on all three images. Output is byte-identical (60/60 decodes in one process under both allocators). The full table is in the commit message.

🤖 Generated with Claude Code

With one output band (set_single_threaded, or one usable core) the image is
allocated zeroed and rows are rendered directly into it, instead of
through a row buffer copied into uninitialised capacity. A zeroed
allocation of this size comes from fresh, already-zero pages under the
Windows heap (and glibc), so the zeroing is free and the per-row copy was
pure overhead: it was what still left the default decoder behind
zune-jpeg on one core under the system allocator after 0.4.5 (ArtCraft:
1.050x photo, 1.009x frame). Several bands keep the uninitialised path,
where a zero-fill would run serially before the bands start.

Under a recycling allocator (rusty_alloc) the memset of reused memory
costs slightly more than the copy did -- ~2% -- and the decoder still
beats zune-jpeg there. One configuration that wins under both allocators,
over one tuned to the allocator it was measured with.

Byte-identical: 30-file fixture set, and all 30 decoded forwards then
backwards in one process (plane reuse across images), single- and
multi-threaded, under both allocators: 60/60 identical to 0.4.3. Clippy
clean on default / no_std / platform_independent / rayon.

Default decode(), pinned to 1 core, ABBA 20x20, wall:

  system allocator   new/0.4.5 (paired)       new/zune-jpeg 0.5.15
  frame1080          0.927, 20/20 (z -4.47)   0.952, 18/20 (z -3.58)
  photo2000s         0.932, 20/20 (z -4.47)   0.917, 20/20 (z -4.47)
  photo2000          0.970, 18/20 (z -3.58)   0.912, 18/20 (z -3.58)

  rusty_alloc        new/0.4.5 (paired)       new/zune-jpeg 0.5.15
  frame1080          1.021,  1/20 (z +4.02)   0.974, 19/20 (z -4.02)
  photo2000s         1.010,  6/20 (z +1.79)   0.933, 20/20 (z -4.47)
  photo2000          1.021,  4/20 (z +2.68)   0.906, 20/20 (z -4.47)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Ttimmahlax
Ttimmahlax merged commit 7938d82 into main Oct 23, 2026
10 checks passed
@Ttimmahlax
Ttimmahlax deleted the perf/single-band-direct branch October 23, 2026 01:54
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.

2 participants