Skip to content

2026-10-04 — libjpeg-turbo, libpng and zlib-ng against stb_image

Question. What does each vendored decoder buy over the stb_image fallback, in time, memory and size?

Answer. - libjpeg-turbo decodes JPEG to gray 1.4–6.4× faster than stb_image, with 30% less peak memory. Decoding at PH_DECODE_SCALE_EIGHTH is a further 1.1–1.6× faster to gray (1.1–2.3× to RGB; least on the progressive file, 1.02–1.07×) and cuts peak memory about ninefold (87 → 10 MB on 20 MP). - libpng + zlib-ng decodes photographic PNG to gray 1.25–1.7× faster than stb_image on Linux and needs 83 MB of peak memory against stb_image's 141 MB (309 MB on macOS) on a 20 MP RGB file. On small screenshots stb_image is as fast. - zlib-ng carries the Linux PNG gain: libpng on the system zlib is only 1.0–1.17× faster than stb_image on x86-64. On macOS, libpng on the system zlib decodes photographs 10–35% faster than on zlib-ng. - Size: the native build's test_benchmark is 0.9–1.3 MB stripped, the stb_image-only one 0.3–0.4 MB.

What was measured

Library code 33c252e
Benchmark code tag bench/2026-10-04-decoders-vs-stb (4c2cca6: scripts/gen_bench_corpus.py, scripts/bench_decoders.sh, test_benchmark taking the decode scale from PH_BENCH_DECODE_SCALE, workflow bench-decoders.yml)
CI run 37172361010, 2026-10-04 02:52 UTC, 7 rounds
Local run 2026-10-04, 5 rounds
Raw data data/2026-10-04-decoders-vs-stb/ (Linux only)
Platform CPU OS Compiler System zlib
Linux x86-64 AMD EPYC 7763 (GitHub ubuntu-latest) Ubuntu 24.04, image 20260927.320.1 GCC 13.3.0 1.3 (1:1.3.dfsg-3.1ubuntu2.2)
Linux arm64 Neoverse-N2 (GitHub ubuntu-24.04-arm) Ubuntu 24.04, image 20260927.135.1 GCC 13.3.0 1.3 (same package)
macOS arm64 Apple M3 Pro, 36 GB macOS 26.6.2 Apple clang 21.0.0 Apple 1.2.12

The x86-64 runner is a different CPU from the one in the libpng-against-spng record of the same day, so absolute times of the two records do not compare; ratios within a record do.

Decoders: libjpeg-turbo 3.2.0 (libjpeg API), libpng 1.6.58, zlib-ng 2.3.3, stb_image 2.30. Three builds of test_benchmark, Release, WebP off: - native — libjpeg-turbo + libpng + zlib-ng; - syszlib — libpng + the system zlib, JPEG through stb_image (PNG rows only); - stb — every native decoder off.

Corpus: the eight PNGs of the libpng-against-spng record, and six JPEGs from the same photograph (Pillow 12.3.0): quality 90 at 20 MP as 4:2:0, 4:4:4, progressive 4:2:0 and gray; quality 85 4:2:0 at 9 MP and at 2 MP.

Method: as in the libpng-against-spng record — test_benchmark --json load, gray and RGB, rotated order, median of min_ms. × is stb_image's time over the native one. JPEG also runs on the native build at PH_DECODE_SCALE_EIGHTH (libjpeg's DCT-domain downscale, which stb_image does not have). Footprint: stripped test_benchmark, and peak RSS (/usr/bin/time) of one process decoding the largest file of each format once.

Linux x86-64

PNG, load to gray

file libpng+zlib-ng libpng+zlib stb zlib-ng vs stb zlib vs stb
photo_gray16_9mp.png 65.94 79.28 83.82 1.27× 1.06×
photo_gray8_20mp.png 85.44 109.91 128.95 1.51× 1.17×
photo_rgb16_9mp.png 217.09 268.89 271.50 1.25× 1.01×
photo_rgb8_20mp.png 190.10 237.16 259.88 1.37× 1.10×
photo_rgb8_2mp.png 19.17 26.58 27.89 1.46× 1.05×
photo_rgba8_9mp.png 99.45 135.85 141.74 1.43× 1.04×
screenshot_pal8_4mp.png 9.84 18.01 7.86 0.80× 0.44×
screenshot_rgb8_4mp.png 8.72 34.94 12.45 1.43× 0.36×

JPEG, load to gray

file libjpeg-turbo libjpeg-turbo ⅛ stb turbo vs stb turbo ⅛ vs stb
q85_420_2mp.jpg 3.39 2.49 14.26 4.21× 5.73×
q85_420_9mp.jpg 19.48 15.63 64.79 3.33× 4.14×
q90_420_20mp.jpg 72.32 61.83 184.15 2.55× 2.98×
q90_420_prog_20mp.jpg 236.93 229.96 413.51 1.75× 1.80×
q90_444_20mp.jpg 77.71 67.53 206.37 2.66× 3.06×
q90_gray_20mp.jpg 72.62 62.92 104.19 1.43× 1.66×

PNG, load to rgb

file libpng+zlib-ng libpng+zlib stb zlib-ng vs stb zlib vs stb
photo_gray16_9mp.png 85.23 98.47 89.52 1.05× 0.91×
photo_gray8_20mp.png 102.63 127.47 145.33 1.42× 1.14×
photo_rgb16_9mp.png 206.99 258.92 262.04 1.27× 1.01×
photo_rgb8_20mp.png 166.65 213.74 238.98 1.43× 1.12×
photo_rgb8_2mp.png 16.64 24.02 25.58 1.54× 1.06×
photo_rgba8_9mp.png 96.90 129.87 136.09 1.40× 1.05×
screenshot_pal8_4mp.png 5.14 13.32 3.59 0.70× 0.27×
screenshot_rgb8_4mp.png 4.01 30.25 8.16 2.04× 0.27×

JPEG, load to rgb

file libjpeg-turbo libjpeg-turbo ⅛ stb turbo vs stb turbo ⅛ vs stb
q85_420_2mp.jpg 4.86 2.71 11.91 2.45× 4.40×
q85_420_9mp.jpg 25.20 16.49 54.93 2.18× 3.33×
q90_420_20mp.jpg 88.31 63.76 163.35 1.85× 2.56×
q90_420_prog_20mp.jpg 249.27 231.46 392.45 1.57× 1.70×
q90_444_20mp.jpg 98.82 71.84 185.16 1.87× 2.58×
q90_gray_20mp.jpg 87.75 63.12 120.77 1.38× 1.91×

Footprint

build stripped test_benchmark peak RSS, largest PNG peak RSS, largest JPEG
native 1307 KB 83764 KB 87744 KB (⅛: 10508 KB)
syszlib 647 KB 83680 KB —
stb 407 KB 141780 KB 125764 KB

Linux arm64

PNG, load to gray

file libpng+zlib-ng libpng+zlib stb zlib-ng vs stb zlib vs stb
photo_gray16_9mp.png 56.62 63.89 75.29 1.33× 1.18×
photo_gray8_20mp.png 81.48 98.52 136.46 1.67× 1.39×
photo_rgb16_9mp.png 149.94 186.18 228.30 1.52× 1.23×
photo_rgb8_20mp.png 174.62 202.83 243.08 1.39× 1.20×
photo_rgb8_2mp.png 16.12 20.59 23.66 1.47× 1.15×
photo_rgba8_9mp.png 90.93 112.67 126.97 1.40× 1.13×
screenshot_pal8_4mp.png 7.22 13.22 7.69 1.07× 0.58×
screenshot_rgb8_4mp.png 8.66 28.83 13.20 1.52× 0.46×

JPEG, load to gray

file libjpeg-turbo libjpeg-turbo ⅛ stb turbo vs stb turbo ⅛ vs stb
q85_420_2mp.jpg 3.40 2.14 21.74 6.39× 10.14×
q85_420_9mp.jpg 19.11 13.52 94.67 4.95× 7.00×
q90_420_20mp.jpg 69.59 53.80 266.59 3.83× 4.95×
q90_420_prog_20mp.jpg 197.47 183.83 430.24 2.18× 2.34×
q90_444_20mp.jpg 75.21 59.48 331.74 4.41× 5.58×
q90_gray_20mp.jpg 70.88 55.13 144.82 2.04× 2.63×

PNG, load to rgb

file libpng+zlib-ng libpng+zlib stb zlib-ng vs stb zlib vs stb
photo_gray16_9mp.png 74.08 81.21 78.85 1.06× 0.97×
photo_gray8_20mp.png 107.09 124.18 160.00 1.49× 1.29×
photo_rgb16_9mp.png 142.66 178.90 221.33 1.55× 1.24×
photo_rgb8_20mp.png 157.88 186.59 228.42 1.45× 1.22×
photo_rgb8_2mp.png 14.29 18.77 21.96 1.54× 1.17×
photo_rgba8_9mp.png 87.92 110.11 124.56 1.42× 1.13×
screenshot_pal8_4mp.png 3.85 9.86 4.64 1.21× 0.47×
screenshot_rgb8_4mp.png 5.30 25.48 10.12 1.91× 0.40×

JPEG, load to rgb

file libjpeg-turbo libjpeg-turbo ⅛ stb turbo vs stb turbo ⅛ vs stb
q85_420_2mp.jpg 5.60 2.42 20.10 3.59× 8.32×
q85_420_9mp.jpg 27.85 14.57 88.05 3.16× 6.04×
q90_420_20mp.jpg 103.32 56.13 251.77 2.44× 4.49×
q90_420_prog_20mp.jpg 222.77 186.41 416.18 1.87× 2.23×
q90_444_20mp.jpg 113.49 62.95 316.41 2.79× 5.03×
q90_gray_20mp.jpg 86.57 55.17 170.14 1.97× 3.08×

Footprint

build stripped test_benchmark peak RSS, largest PNG peak RSS, largest JPEG
native 1027 KB 83104 KB 86888 KB (⅛: 9980 KB)
syszlib 579 KB 83016 KB —
stb 323 KB 141260 KB 125268 KB

macOS arm64

PNG, load to gray

file libpng+zlib-ng libpng+zlib stb zlib-ng vs stb zlib vs stb
photo_gray16_9mp.png 41.16 30.66 56.07 1.36× 1.83×
photo_gray8_20mp.png 76.70 63.22 118.62 1.55× 1.88×
photo_rgb16_9mp.png 136.74 124.28 152.31 1.11× 1.23×
photo_rgb8_20mp.png 122.45 90.89 193.35 1.58× 2.13×
photo_rgb8_2mp.png 10.80 8.41 16.20 1.50× 1.93×
photo_rgba8_9mp.png 65.73 51.96 102.35 1.56× 1.97×
screenshot_pal8_4mp.png 4.37 5.17 4.18 0.96× 0.81×
screenshot_rgb8_4mp.png 6.46 8.32 6.36 0.99× 0.76×

JPEG, load to gray

file libjpeg-turbo libjpeg-turbo ⅛ stb turbo vs stb turbo ⅛ vs stb
q85_420_2mp.jpg 2.08 1.55 9.69 4.66× 6.26×
q85_420_9mp.jpg 12.62 10.14 44.83 3.55× 4.42×
q90_420_20mp.jpg 47.74 41.37 128.32 2.69× 3.10×
q90_420_prog_20mp.jpg 141.63 139.28 263.60 1.86× 1.89×
q90_444_20mp.jpg 50.88 46.27 147.78 2.90× 3.19×
q90_gray_20mp.jpg 47.63 42.23 82.55 1.73× 1.95×

PNG, load to rgb

file libpng+zlib-ng libpng+zlib stb zlib-ng vs stb zlib vs stb
photo_gray16_9mp.png 49.53 38.44 58.85 1.19× 1.53×
photo_gray8_20mp.png 84.06 70.48 128.40 1.53× 1.82×
photo_rgb16_9mp.png 131.05 119.89 146.80 1.12× 1.22×
photo_rgb8_20mp.png 111.42 79.83 184.69 1.66× 2.31×
photo_rgb8_2mp.png 9.59 6.90 15.19 1.58× 2.20×
photo_rgba8_9mp.png 63.47 49.74 101.45 1.60× 2.04×
screenshot_pal8_4mp.png 1.98 2.68 2.14 1.08× 0.80×
screenshot_rgb8_4mp.png 4.06 5.90 4.33 1.07× 0.73×

JPEG, load to rgb

file libjpeg-turbo libjpeg-turbo ⅛ stb turbo vs stb turbo ⅛ vs stb
q85_420_2mp.jpg 2.68 1.72 8.47 3.16× 4.93×
q85_420_9mp.jpg 15.52 10.89 40.11 2.58× 3.68×
q90_420_20mp.jpg 53.83 42.93 117.72 2.19× 2.74×
q90_420_prog_20mp.jpg 147.44 140.60 252.33 1.71× 1.79×
q90_444_20mp.jpg 59.38 47.42 137.54 2.32× 2.90×
q90_gray_20mp.jpg 48.32 42.48 90.60 1.88× 2.13×

Footprint

build stripped test_benchmark peak RSS, largest PNG peak RSS, largest JPEG
native 936 KB 81408 KB 86704 KB (⅛: 9440 KB)
syszlib 513 KB 81264 KB —
stb 327 KB 309072 KB 125280 KB