perf: optimize horizontal and float predictor decode

Horizontal predictor (one_byte_predict, all integer TIFFs with
Predictor=2): rewrite with explicit [u8; N] accumulator so LLVM unrolls
and keeps each channel in a register. For N=3 (RGB8), this replaces a
scalar 1-byte-per-iteration loop with a 2x-unrolled 3-register loop.

Float predictor (Predictor=3, f16/f32/f64): split deinterleave into
per-format functions with plane slicing up front; f32 batches 4 samples
through a u128 wide store to eliminate per-sample bounds checks. Thread
a reusable scratch buffer through expand_chunk to eliminate per-row
heap allocation.

Benchmarked on real 21MP photos and GeoTIFF DEMs (vs upstream main):

  Strip RGB8 deflate+hpred:      253ms -> 224ms (-11%)
  Strip RGB8 LZW:                405ms -> 370ms (-9%)
  Tiled RGB8 deflate+hpred:      241ms -> 222ms (-8%)
  Strip RGB f32 deflate+fpred:   726ms -> 527ms (-27%)
  Gray f32 tiled 512:            290ms -> 166ms (-43%)
  Copernicus DEM GeoTIFF:        239ms -> 152ms (-37%)
  USGS DEM GeoTIFF:              284ms -> 198ms (-30%)
This commit is contained in:
Lilith River committed 2026-04-06 08:49:00 -06:00
1 parent dc94cd4436
commit fa8a911f79
2 files changed
+197 -58

No files matched your search

+5
View File
@@ -1009,6 +1009,7 @@ impl Image {
buf: &mut [u8],
layout: &ReadoutLayout,
chunk_index: u32,
scratch: &mut Vec<u8>,
) -> TiffResult<()> {
let ValueReader {
reader,
@@ -1191,6 +1192,7 @@ impl Image {
samples,
ByteOrder::native(),
predictor,
scratch,
);
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
@@ -1209,6 +1211,7 @@ impl Image {
samples,
decompressed_byte_order,
predictor,
scratch,
);
}
@@ -1255,6 +1258,7 @@ impl Image {
samples,
decompressed_byte_order,
predictor,
scratch,
);
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
@@ -1296,6 +1300,7 @@ impl Image {
samples,
decompressed_byte_order,
predictor,
scratch,
);
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {