Skip to content

perf(decode): reuse component planes across decodes (system-allocator gap, remade_ffmpeg_rs#48) - #15

Merged
Ttimmahlax merged 1 commit into
mainfrom
perf/plane-reuse
Oct 23, 2026
Merged

Ttimmahlax merged 1 commit into
mainfrom
perf/plane-reuse

Conversation

@Ttimmahlax

Copy link
Copy Markdown
Contributor

ArtCraft found that 0.4.4 is still behind zune-jpeg on one core under the system allocator: 1.13–1.15x on the photo and 1.22–1.24x on the 1080p frame. My earlier numbers were all under rusty_alloc, which hid it. Reproduced here: 1.13x and 1.17x, 16/16 rounds.

Cause: with a fresh Decoder per image, the ~3.1 MB of component planes per 1080p decode page-fault on every decode under the Windows heap (+0.43 ms).

Fix: compute_image returns the planes to a bounded thread-local pool, and the next decode reuses them.

  • The pool holds at most 4 planes and 64 MB.
  • RUSTY_JPEG_PLANE_POOL=0 disables it.
  • It's a no-op without std.

Correctness: byte-identical to 0.4.3. That includes 60 decodes run in one process, so pooled planes cross between images of different sizes and subsamplings, under both allocators.

default decode(), 1 core vs 0.4.4 vs zune-jpeg
system alloc, frame1080 0.86x (19/20) 1.02x (was 1.17x)
system alloc, photo2000s 0.88x (20/20) 0.98x (was 1.13x)
system alloc, photo2000 0.95x (19/20) 0.95x
rusty_alloc, frame1080 0.96x 0.96x
rusty_alloc, photo2000s 0.99x 0.89x
rusty_alloc, photo2000 neutral 0.93x

🤖 Generated with Claude Code

A decode to RGB sized one plane per component (~3.1 MB for a 1080p 4:2:0
frame) and dropped them as soon as the image was assembled, so a caller
that builds a fresh Decoder per image -- thumbnailers, MJPEG frame loops,
the ArtCraft apps in remade_ffmpeg_rs#48 -- paid for those pages to be
faulted in on every decode. Under rusty_alloc/mimalloc that was nearly
free; under the Windows system heap it was +0.43 ms per 1080p frame and
left 0.4.4 1.13-1.17x behind zune-jpeg on one core (reported by ArtCraft,
reproduced 16/16).

`compute_image` now borrows the planes and hands them to a small
thread-local pool (`decode::plane_pool`); the next decode on the thread
takes the best-fitting one before allocating. Bounded to 4 planes and 64
MB (an oversized image's planes are not kept), disabled with
RUSTY_JPEG_PLANE_POOL=0, a no-op without std. Planes are refilled, never
trusted: every block is written before a plane is read, which the
cross-image check confirms.

Byte-identical: 30-file fixture set per file, plus all 30 decoded
forwards then backwards in ONE process (planes passing between images of
different sizes and subsamplings), single- and multi-threaded, under both
allocators: 60/60 identical to 0.4.3.

Measured vs 0.4.4, default decode() pinned to 1 core, ABBA 20x20, wall:

  system allocator   new/0.4.4 (paired)       new/zune-jpeg 0.5.15
  frame1080          0.863, 19/20 (z -4.02)   1.016 (zune 16/20)
  photo2000s         0.883, 20/20 (z -4.47)   0.978, 17/20
  photo2000          0.945, 19/20 (z -4.02)   0.946, 19/20

  rusty_alloc        new/0.4.4 (paired)       new/zune-jpeg 0.5.15
  frame1080          0.964, 14/20 (z -1.79)   0.956, 17/20
  photo2000s         0.986, 14/20 (z -1.79)   0.892, 20/20
  photo2000          1.001, 10/20 (neutral)   0.932, 17/20

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Ttimmahlax
Ttimmahlax merged commit 512be6d into main Oct 23, 2026
10 checks passed
@Ttimmahlax
Ttimmahlax deleted the perf/plane-reuse branch October 23, 2026 00:49
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