From eceaa1f14dc7d395e93a9b026a26c989ee25f41e Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Feb 2026 00:27:40 +0000 Subject: [PATCH 1/3] Add encoding benchmark for RGB32Float with float predictor and DEFLATE Co-Authored-By: Claude --- Cargo.toml | 4 ++++ benches/encode.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 benches/encode.rs diff --git a/Cargo.toml b/Cargo.toml index c1a7f0f..d1531f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,10 @@ zstd = ["dep:zstd"] name = "lzw" harness = false +[[bench]] +name = "encode" +harness = false + [[test]] name = "decode_libtiffpic" path = "tests/decode_libtiffpic.rs" diff --git a/benches/encode.rs b/benches/encode.rs new file mode 100644 index 0000000..23b2cf9 --- /dev/null +++ b/benches/encode.rs @@ -0,0 +1,44 @@ +extern crate criterion; +extern crate tiff; + +use criterion::{black_box, Criterion, Throughput}; +use std::io::Cursor; +use tiff::encoder::colortype::RGB32Float; +use tiff::encoder::{Compression, DeflateLevel, Predictor, TiffEncoder}; + +const WIDTH: u32 = 500; +const HEIGHT: u32 = 500; + +fn encode_image_f32_float_predictor_deflate(data: &[f32]) { + let mut buf = Cursor::new(Vec::with_capacity(WIDTH as usize * HEIGHT as usize * 4)); + TiffEncoder::new(&mut buf) + .unwrap() + .with_predictor(Predictor::FloatingPoint) + .with_compression(Compression::Deflate(DeflateLevel::Fast)) + .write_image::(WIDTH, HEIGHT, black_box(data)) + .unwrap(); +} + +fn main() { + // Generate synthetic f32 image data with a gradient pattern + let data: Vec = (0..WIDTH as usize * HEIGHT as usize * 3) + .map(|i| { + let pixel = i / 3; + let x = (pixel % WIDTH as usize) as f32 / WIDTH as f32; + let y = (pixel / WIDTH as usize) as f32 / HEIGHT as f32; + x * 0.5 + y * 0.5 + }) + .collect(); + + let mut c = Criterion::default().configure_from_args(); + let mut group = c.benchmark_group("tiff-encode"); + + group + .sample_size(20) + .throughput(Throughput::Bytes( + (WIDTH as u64) * (HEIGHT as u64) * 3 * std::mem::size_of::() as u64, + )) + .bench_function("f32-float-predictor-deflate-fast", |b| { + b.iter(|| encode_image_f32_float_predictor_deflate(&data)) + }); +} From 2c5fe2c2f5be3e4e5ccd88f9a5d26500c59998a7 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Feb 2026 00:47:13 +0000 Subject: [PATCH 2/3] Use chunked writes for byte shuffle in fp_predict Replace single-byte pushes with chunk-based processing: pre-allocate zeroed output, shuffle 4 values at a time into a stack buffer, then copy_from_slice each byte lane into the output. Co-Authored-By: Claude --- src/encoder/colortype.rs | 41 +++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index ef4b9a8..0811ef3 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -18,15 +18,46 @@ macro_rules! fp_predict { let byte_count = num_values * BYTE_SIZE; let start_len = result.len(); - // Reserve space for the output - result.reserve(byte_count); + // Allocate zeroed space so we can write to arbitrary positions + result.resize(start_len + byte_count, 0); + let output = &mut result[start_len..]; // Step 1 & 2: Convert to big-endian and shuffle bytes // All first bytes, then all second bytes, etc. - for byte_idx in 0..BYTE_SIZE { - for &value in row { + // + // We process the input in chunks of 4 floats/doubles at a time. + // For each chunk we shuffle the bytes into a stack buffer, then + // copy each byte-lane's portion into the output. This turns + // single-byte pushes into 16-byte (for f32) or 32-byte (for f64) + // memcpy-like stores that the compiler can vectorize. + const CHUNK: usize = 4; + let chunks = num_values / CHUNK; + let remainder = num_values % CHUNK; + + let mut scratch = [[0u8; CHUNK]; BYTE_SIZE]; + for c in 0..chunks { + let src = &row[c * CHUNK..][..CHUNK]; + for j in 0..CHUNK { + let bytes = src[j].to_be_bytes(); + for b in 0..BYTE_SIZE { + scratch[b][j] = bytes[b]; + } + } + for b in 0..BYTE_SIZE { + let dest_offset = b * num_values + c * CHUNK; + output[dest_offset..dest_offset + CHUNK] + .copy_from_slice(&scratch[b]); + } + } + + // Handle the remaining elements that don't fill a full chunk + if remainder > 0 { + let src = &row[chunks * CHUNK..]; + for (j, &value) in src.iter().enumerate() { let bytes = value.to_be_bytes(); - result.push(bytes[byte_idx]); + for b in 0..BYTE_SIZE { + output[b * num_values + chunks * CHUNK + j] = bytes[b]; + } } } From 22046d42725ca1b220f48fae17416b8145b46ade Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Feb 2026 00:49:28 +0000 Subject: [PATCH 3/3] Simplify byte shuffle to direct scatter writes Replace chunked scratch-buffer approach with direct indexed writes into the pre-allocated output. Simpler and faster in practice. Co-Authored-By: Claude --- src/encoder/colortype.rs | 39 ++++----------------------------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index 0811ef3..189d80c 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -22,42 +22,11 @@ macro_rules! fp_predict { result.resize(start_len + byte_count, 0); let output = &mut result[start_len..]; - // Step 1 & 2: Convert to big-endian and shuffle bytes - // All first bytes, then all second bytes, etc. - // - // We process the input in chunks of 4 floats/doubles at a time. - // For each chunk we shuffle the bytes into a stack buffer, then - // copy each byte-lane's portion into the output. This turns - // single-byte pushes into 16-byte (for f32) or 32-byte (for f64) - // memcpy-like stores that the compiler can vectorize. - const CHUNK: usize = 4; - let chunks = num_values / CHUNK; - let remainder = num_values % CHUNK; - - let mut scratch = [[0u8; CHUNK]; BYTE_SIZE]; - for c in 0..chunks { - let src = &row[c * CHUNK..][..CHUNK]; - for j in 0..CHUNK { - let bytes = src[j].to_be_bytes(); - for b in 0..BYTE_SIZE { - scratch[b][j] = bytes[b]; - } - } + // Step 1 & 2: Convert to big-endian and scatter bytes + for (i, &value) in row.iter().enumerate() { + let bytes = value.to_be_bytes(); for b in 0..BYTE_SIZE { - let dest_offset = b * num_values + c * CHUNK; - output[dest_offset..dest_offset + CHUNK] - .copy_from_slice(&scratch[b]); - } - } - - // Handle the remaining elements that don't fill a full chunk - if remainder > 0 { - let src = &row[chunks * CHUNK..]; - for (j, &value) in src.iter().enumerate() { - let bytes = value.to_be_bytes(); - for b in 0..BYTE_SIZE { - output[b * num_values + chunks * CHUNK + j] = bytes[b]; - } + output[b * num_values + i] = bytes[b]; } }