Skip to content

2026-10-04 — PNG decoding: libpng against spng

Question. Is spng, as an alternative PNG backend, faster than libpng?

Answer. No. On 8-bit color photographs spng is within a few percent of libpng, at best 8.6% faster (x86-64, 20 MP RGB decoded to gray); its largest gain, 17%, is 0.7 ms on a 48 KB palette screenshot decoded to RGB. On 16-bit and grayscale images it is 30–70% slower on every platform. spng is not a backend of the library.

What was measured

Library code 32d84b9
Benchmark code tag bench/2026-10-04-png-libpng-vs-spng (ba2228b: scripts/gen_png_corpus.py, scripts/bench_png_backends.sh, workflow bench-png-backends.yml)
CI run 37169860325, 2026-10-04 02:03 UTC, 7 rounds
Local run 2026-10-04, 5 rounds
Raw data data/2026-10-04-png-libpng-vs-spng/ (Linux only)
Platform CPU OS Compiler System zlib
Linux x86-64 AMD EPYC 9V45 (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

Decoders: libpng 1.6.58, spng 0.7.4, zlib-ng 2.3.3. Four builds of test_benchmark, Release, JPEG and WebP off: {libpng, spng} × {zlib-ng, system zlib}.

Corpus: eight PNGs made from tests/data/photo_large.jpeg (5472×3648) with Pillow 12.3.0 and numpy 2.5.3: RGB 8-bit at 20 and 2 MP, gray 8-bit at 20 MP, RGBA 8-bit, gray 16-bit and RGB 16-bit at 9 MP, a 2560×1600 synthetic screenshot as RGB and as a 64-color palette. Compression level 6. File sizes differ between the CI and the local tables: the same Pillow version, built for a different platform.

Method: test_benchmark --json load <file> <iters>, a new context per load, decode to gray and to RGB. Each run reports min_ms over its iterations; the order of the builds rotates every round; a cell is the median of min_ms across rounds. spng gain is (libpng − spng) / libpng: above zero, spng is faster.

Linux x86-64

Load to gray

file size libpng+zlib-ng spng+zlib-ng spng gain libpng+zlib spng+zlib spng gain
photo_gray16_9mp.png 7.3 MB 45.966374 68.087936 -48.1% 55.793856 77.837339 -39.5%
photo_gray8_20mp.png 13.5 MB 57.852149 62.099001 -7.3% 76.559404 81.076831 -5.9%
photo_rgb16_9mp.png 9.7 MB 139.364864 183.180107 -31.4% 178.302084 216.262920 -21.3%
photo_rgb8_20mp.png 22.0 MB 132.328228 120.915501 +8.6% 159.609624 154.550397 +3.2%
photo_rgb8_2mp.png 1.9 MB 13.551712 13.037715 +3.8% 18.553292 18.675946 -0.7%
photo_rgba8_9mp.png 10.0 MB 67.706968 65.959019 +2.6% 90.775267 89.311434 +1.6%
screenshot_pal8_4mp.png 0.0 MB 6.624196 6.026954 +9.0% 11.345661 12.036174 -6.1%
screenshot_rgb8_4mp.png 0.1 MB 7.393539 7.957246 -7.6% 26.422148 28.206032 -6.8%

Load to rgb

file size libpng+zlib-ng spng+zlib-ng spng gain libpng+zlib spng+zlib spng gain
photo_gray16_9mp.png 7.3 MB 57.015579 61.818436 -8.4% 66.308776 71.402115 -7.7%
photo_gray8_20mp.png 13.5 MB 67.692058 95.886028 -41.7% 86.576319 108.185925 -25.0%
photo_rgb16_9mp.png 9.7 MB 131.445416 176.534654 -34.3% 173.022316 209.824943 -21.3%
photo_rgb8_20mp.png 22.0 MB 114.625336 108.887054 +5.0% 147.490921 142.703431 +3.2%
photo_rgb8_2mp.png 1.9 MB 11.555171 11.483814 +0.6% 17.187780 17.132186 +0.3%
photo_rgba8_9mp.png 10.0 MB 65.699002 64.149607 +2.4% 90.570594 88.946308 +1.8%
screenshot_pal8_4mp.png 0.0 MB 2.974047 2.887798 +2.9% 8.888123 8.811286 +0.9%
screenshot_rgb8_4mp.png 0.1 MB 3.759998 4.782124 -27.2% 23.966215 25.020315 -4.4%

Linux arm64

Load to gray

file size libpng+zlib-ng spng+zlib-ng spng gain libpng+zlib spng+zlib spng gain
photo_gray16_9mp.png 7.3 MB 56.857883 91.175457 -60.4% 64.098447 98.670673 -53.9%
photo_gray8_20mp.png 13.5 MB 81.812884 88.551673 -8.2% 98.618592 104.659291 -6.1%
photo_rgb16_9mp.png 9.7 MB 150.333375 242.856047 -61.5% 186.497250 277.767864 -48.9%
photo_rgb8_20mp.png 22.0 MB 175.905809 168.979197 +3.9% 203.968648 197.272343 +3.3%
photo_rgb8_2mp.png 1.9 MB 16.268353 17.355461 -6.7% 20.716749 21.931227 -5.9%
photo_rgba8_9mp.png 10.0 MB 92.478114 90.219424 +2.4% 114.201652 112.276707 +1.7%
screenshot_pal8_4mp.png 0.0 MB 7.456869 9.995747 -34.0% 13.550108 16.312986 -20.4%
screenshot_rgb8_4mp.png 0.1 MB 8.733986 11.655247 -33.4% 29.096888 32.330842 -11.1%

Load to rgb

file size libpng+zlib-ng spng+zlib-ng spng gain libpng+zlib spng+zlib spng gain
photo_gray16_9mp.png 7.3 MB 74.205831 76.712106 -3.4% 81.335424 84.156212 -3.5%
photo_gray8_20mp.png 13.5 MB 108.886136 140.913885 -29.4% 125.858931 157.169137 -24.9%
photo_rgb16_9mp.png 9.7 MB 142.917388 228.423832 -59.8% 179.139540 263.617332 -47.2%
photo_rgb8_20mp.png 22.0 MB 159.571976 153.133525 +4.0% 187.357716 181.364692 +3.2%
photo_rgb8_2mp.png 1.9 MB 14.414841 14.021745 +2.7% 18.892449 18.633246 +1.4%
photo_rgba8_9mp.png 10.0 MB 89.807916 87.422123 +2.7% 111.516589 109.486476 +1.8%
screenshot_pal8_4mp.png 0.0 MB 4.059531 3.373617 +16.9% 10.179321 9.412244 +7.5%
screenshot_rgb8_4mp.png 0.1 MB 5.350807 5.285900 +1.2% 25.644186 25.568007 +0.3%

macOS arm64

Load to gray

file size libpng+zlib-ng spng+zlib-ng spng gain libpng+zlib spng+zlib spng gain
photo_gray16_9mp.png 6.5 MB 41.725208 70.497667 -69.0% 31.033167 59.999125 -93.3%
photo_gray8_20mp.png 13.6 MB 76.781167 99.509958 -29.6% 63.298375 85.984167 -35.8%
photo_rgb16_9mp.png 9.7 MB 139.409875 143.486625 -2.9% 126.511042 130.981542 -3.5%
photo_rgb8_20mp.png 20.4 MB 124.414625 123.588500 +0.7% 92.972083 92.457458 +0.6%
photo_rgb8_2mp.png 1.8 MB 10.820458 10.933792 -1.0% 8.483708 8.552167 -0.8%
photo_rgba8_9mp.png 9.3 MB 65.851167 61.816250 +6.1% 52.308208 48.485875 +7.3%
screenshot_pal8_4mp.png 0.0 MB 4.376667 4.606042 -5.2% 5.143583 5.395917 -4.9%
screenshot_rgb8_4mp.png 0.1 MB 6.473833 6.835833 -5.6% 8.413875 8.757750 -4.1%

Load to rgb

file size libpng+zlib-ng spng+zlib-ng spng gain libpng+zlib spng+zlib spng gain
photo_gray16_9mp.png 6.5 MB 49.913750 63.982667 -28.2% 39.169917 53.277542 -36.0%
photo_gray8_20mp.png 13.6 MB 84.008917 133.039333 -58.4% 70.598333 119.638833 -69.5%
photo_rgb16_9mp.png 9.7 MB 134.408750 136.481625 -1.5% 120.590625 123.582083 -2.5%
photo_rgb8_20mp.png 20.4 MB 112.559042 108.755292 +3.4% 80.642000 77.440625 +4.0%
photo_rgb8_2mp.png 1.8 MB 9.485083 9.316500 +1.8% 7.182875 6.988792 +2.7%
photo_rgba8_9mp.png 9.3 MB 63.443042 61.454625 +3.1% 49.917042 48.301167 +3.2%
screenshot_pal8_4mp.png 0.0 MB 2.020750 1.792542 +11.3% 2.809500 2.506500 +10.8%
screenshot_rgb8_4mp.png 0.1 MB 4.035208 3.864542 +4.2% 5.905833 5.618000 +4.9%