From 7fb54c6dd3a971de3cd001a0d8e02eb0da79e639 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Thu, 10 Sep 2026 08:29:46 +0200 Subject: [PATCH 1/2] bench: reuse the buffers across iterations Every iteration wrote into, or read into, a freshly allocated Vec, and for the large sizes the kernel mapping that memory cost several times the copy itself. Create the file buffer, the source data and the read sink once per case so the numbers cover the crate. --- benches/benchmark.rs | 91 ++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 23113e5..3962702 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -10,22 +10,29 @@ use std::io::Write; use std::time::Duration; use tempfile::NamedTempFile; +// The buffers a case writes into or reads into are created once per case and +// reused across iterations, so that the measurements cover the crate rather +// than the kernel mapping fresh memory: for the large sizes, filling a newly +// allocated Vec costs several times the copy itself. + +/// Writes `n` streams of `data` into `buff`, reusing its allocation. +fn write_many_streams_into(buff: &mut Vec, data: &[u8], n: usize) { + buff.clear(); + let mut test_comp = CompoundFile::create(Cursor::new(buff)).unwrap(); + for i in 0..n { + let name = format!("test{i}"); + let mut stream = test_comp.create_stream(name).unwrap(); + stream.write_all(data).unwrap(); + } +} + fn write_many_streams(n: usize, size: usize) -> Vec { let mut buff = Vec::new(); - { - let mut test_comp = - CompoundFile::create(Cursor::new(&mut buff)).unwrap(); - let data = vec![0; size]; - for i in 0..n { - let name = format!("test{i}"); - let mut stream = test_comp.create_stream(name).unwrap(); - stream.write_all(&data).unwrap(); - } - } + write_many_streams_into(&mut buff, &vec![0; size], n); buff } -fn write_many_streams_to_file(tmpfile: &NamedTempFile, n: usize, size: usize) { +fn write_many_streams_to_file(tmpfile: &NamedTempFile, data: &[u8], n: usize) { let mut test_comp = CompoundFile::create( OpenOptions::new() .read(true) @@ -34,41 +41,45 @@ fn write_many_streams_to_file(tmpfile: &NamedTempFile, n: usize, size: usize) { .unwrap(), ) .unwrap(); - let data = vec![0; size]; for i in 0..n { let name = format!("test{i}"); let mut stream = test_comp.create_stream(name).unwrap(); - stream.write_all(&data).unwrap(); + stream.write_all(data).unwrap(); } } -fn write_many_streams_disk(n: usize, size: usize) { +fn write_many_streams_disk(data: &[u8], n: usize) { let tmpfile = NamedTempFile::new().unwrap(); - write_many_streams_to_file(&tmpfile, n, size); + write_many_streams_to_file(&tmpfile, data, n); // File is deleted when tmpfile is dropped } -fn read_many_streams_disk(tmpfile: &NamedTempFile, n: usize) { - // An unbuffered File, as `cfb::open` hands out. - let mut test_comp = - CompoundFile::open(File::open(tmpfile.path()).unwrap()).unwrap(); +/// Reads every stream into `sink`, reusing its allocation. +fn read_many_streams(buff: &[u8], n: usize, sink: &mut Vec) { + let mut test_comp = CompoundFile::open(Cursor::new(buff)).unwrap(); for i in 0..n { let name = format!("test{i}"); let mut stream = test_comp.open_stream(name).unwrap(); - let mut sink = Vec::new(); - stream.read_to_end(&mut sink).unwrap(); - black_box(sink); + sink.clear(); + stream.read_to_end(sink).unwrap(); + black_box(&sink); } } -fn read_many_streams(buff: &[u8], n: usize) { - let mut test_comp = CompoundFile::open(Cursor::new(buff)).unwrap(); +fn read_many_streams_disk( + tmpfile: &NamedTempFile, + n: usize, + sink: &mut Vec, +) { + // An unbuffered File, as `cfb::open` hands out. + let mut test_comp = + CompoundFile::open(File::open(tmpfile.path()).unwrap()).unwrap(); for i in 0..n { let name = format!("test{i}"); let mut stream = test_comp.open_stream(name).unwrap(); - let mut sink = Vec::new(); - stream.read_to_end(&mut sink).unwrap(); - black_box(sink); + sink.clear(); + stream.read_to_end(sink).unwrap(); + black_box(&sink); } } @@ -90,13 +101,15 @@ fn criterion_benchmark(c: &mut Criterion) { if total_bytes > 0 { group.throughput(Throughput::Bytes(total_bytes)); } + let data = vec![0u8; stream_size]; + let mut buff = Vec::new(); group.bench_function(label, |b| { b.iter(|| { - let out = write_many_streams( + write_many_streams_into( + black_box(&mut buff), + black_box(&data), black_box(stream_count), - black_box(stream_size), ); - black_box(out); }) }); } @@ -109,11 +122,12 @@ fn criterion_benchmark(c: &mut Criterion) { if total_bytes > 0 { disk_group.throughput(Throughput::Bytes(total_bytes)); } + let data = vec![0u8; stream_size]; disk_group.bench_function(label, |b| { b.iter(|| { write_many_streams_disk( + black_box(&data), black_box(stream_count), - black_box(stream_size), ); }) }); @@ -128,9 +142,14 @@ fn criterion_benchmark(c: &mut Criterion) { if total_bytes > 0 { read_group.throughput(Throughput::Bytes(total_bytes)); } + let mut sink = Vec::new(); read_group.bench_function(label, |b| { b.iter(|| { - read_many_streams(black_box(&buff), black_box(stream_count)); + read_many_streams( + black_box(&buff), + black_box(stream_count), + &mut sink, + ); }) }); } @@ -140,7 +159,12 @@ fn criterion_benchmark(c: &mut Criterion) { for (label, stream_size, stream_count) in stream_benches { let total_bytes = (stream_count * stream_size) as u64; let tmpfile = NamedTempFile::new().unwrap(); - write_many_streams_to_file(&tmpfile, stream_count, stream_size); + write_many_streams_to_file( + &tmpfile, + &vec![0; stream_size], + stream_count, + ); + let mut sink = Vec::new(); // Disk cases are slow per iteration; keep the group's run time down. read_disk_group.sample_size(10); read_disk_group.warm_up_time(Duration::from_secs(1)); @@ -153,6 +177,7 @@ fn criterion_benchmark(c: &mut Criterion) { read_many_streams_disk( black_box(&tmpfile), black_box(stream_count), + &mut sink, ); }) }); From 68b9d8dec308d9f06d7eba41391eefebe9ffc226 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Thu, 10 Sep 2026 08:43:02 +0200 Subject: [PATCH 2/2] bench: use 256 x 1 MiB so it carries the same bytes as 1 x 256 MiB The two rows then read directly against each other: what separates them is the per-stream overhead and the cache behaviour of 1 MiB buffers. --- benches/benchmark.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 3962702..28500d6 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -90,7 +90,7 @@ fn criterion_benchmark(c: &mut Criterion) { ("n=1000,size=64B", 64usize, 1000usize), ("n=100,size=4KiB-1 (MiniFAT)", 1024 * 4 - 1, 100usize), ("n=100,size=4KiB (FAT)", 1024 * 4, 100usize), - ("n=50,size=1MiB", 1024 * 1024usize, 50usize), + ("n=256,size=1MiB", 1024 * 1024usize, 256usize), ("n=1,size=256MiB", 256 * 1024 * 1024usize, 1usize), ];