From 2e50b0ebc2003321a8b8d293e1bf601acbde3698 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Sun, 20 Sep 2026 16:59:18 +0200 Subject: [PATCH 1/2] bench: open a file from disk Opening loads the DIFAT, FAT, directory and MiniFAT. The existing disk benches include it, but mixed with the stream reads. --- benches/benchmark.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 28500d6..7971779 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -183,6 +183,28 @@ fn criterion_benchmark(c: &mut Criterion) { }); } read_disk_group.finish(); + + // Opening loads the DIFAT, FAT, directory and MiniFAT. + let mut open_disk_group = c.benchmark_group("open_disk"); + for (label, stream_size, stream_count) in stream_benches { + let tmpfile = NamedTempFile::new().unwrap(); + write_many_streams_to_file( + &tmpfile, + &vec![0; stream_size], + stream_count, + ); + open_disk_group.sample_size(10); + open_disk_group.warm_up_time(Duration::from_secs(1)); + open_disk_group.measurement_time(Duration::from_secs(2)); + open_disk_group.bench_function(label, |b| { + b.iter(|| { + // An unbuffered File, as `cfb::open` hands out. + let file = File::open(tmpfile.path()).unwrap(); + black_box(CompoundFile::open(file).unwrap()); + }) + }); + } + open_disk_group.finish(); } /// Reads one large stream the way a caller that streams it out would: in From b8b8ed4b5ea1dee500f908d290b7364ee9c4f154 Mon Sep 17 00:00:00 2001 From: Francis De Brabandere Date: Sun, 20 Sep 2026 16:59:55 +0200 Subject: [PATCH 2/2] perf: read the DIFAT, FAT and MiniFAT in whole sectors on open Opening a file read these tables one 4 byte entry at a time, and for the MiniFAT with a seek before every entry. On an unbuffered File that is one or two syscalls per entry: opening a 100 MB file took 42k reads and 15k seeks before any stream was touched. Read each DIFAT and FAT sector, and the whole MiniFAT chain, into a buffer and decode the entries from memory. The open_disk bench is 70 to 92% faster for the cases that have a FAT or MiniFAT of any size. --- src/lib.rs | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c1e6380..2889897 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -463,6 +463,11 @@ impl CompoundFile { let mut sectors = Sectors::new(header.version, inner_len, inner); let num_sectors = sectors.num_sectors(); + // The DIFAT and FAT are read a whole sector at a time, rather than + // with one tiny read per entry. + let entries_per_sector = sector_len / size_of::(); + let mut sector_buf = vec![0u8; sector_len]; + // Read in DIFAT. let mut difat = Vec::::new(); difat.extend_from_slice(&header.initial_difat_entries); @@ -493,9 +498,11 @@ impl CompoundFile { } seen_sector_ids.insert(current_difat_sector); difat_sector_ids.push(current_difat_sector); - let mut sector = sectors.seek_to_sector(current_difat_sector)?; - for _ in 0..(sector_len / size_of::() - 1) { - let next = sector.read_le_u32()?; + sectors + .seek_to_sector(current_difat_sector)? + .read_exact(&mut sector_buf)?; + let mut entries = le_u32s(§or_buf); + for next in entries.by_ref().take(entries_per_sector - 1) { if next != consts::FREE_SECTOR && next > consts::MAX_REGULAR_SECTOR { @@ -506,7 +513,7 @@ impl CompoundFile { } difat.push(next); } - current_difat_sector = sector.read_le_u32()?; + current_difat_sector = entries.next().unwrap(); if validation.is_strict() && current_difat_sector == consts::FREE_SECTOR { @@ -563,10 +570,10 @@ impl CompoundFile { num_sectors ); } - let mut sector = sectors.seek_to_sector(sector_index)?; - for _ in 0..(sector_len / size_of::()) { - fat.push(sector.read_le_u32()?); - } + sectors + .seek_to_sector(sector_index)? + .read_exact(&mut sector_buf)?; + fat.extend(le_u32s(§or_buf)); } // If the number of sectors in the file is not a multiple of the number // of FAT entries per sector, then the last FAT sector must be padded @@ -677,11 +684,12 @@ impl CompoundFile { chain.num_sectors() ); } + // Read the whole MiniFAT in one go, rather than with one tiny + // read (and a seek) per entry. let num_minifat_entries = (chain.len() / 4) as usize; - let mut minifat = Vec::::with_capacity(num_minifat_entries); - for _ in 0..num_minifat_entries { - minifat.push(chain.read_le_u32()?); - } + let mut minifat_buf = vec![0u8; num_minifat_entries * 4]; + chain.read_exact(&mut minifat_buf)?; + let mut minifat: Vec = le_u32s(&minifat_buf).collect(); while minifat.last() == Some(&consts::FREE_SECTOR) { minifat.pop(); } @@ -1214,6 +1222,13 @@ trait WriteLeNumber: Write { } } impl WriteLeNumber for T {} + +/// Decodes a buffer of little-endian u32 values. +fn le_u32s(buf: &[u8]) -> impl Iterator + '_ { + buf.chunks_exact(size_of::()) + .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])) +} + //===========================================================================// #[cfg(test)]