Skip to content

Commit eca2f55

Browse files
committed
util: hash through std::io::copy, without zeroing a buffer
The 16KB stack buffer is zeroed for every input hashed, which was 45% of the hash_multiple_files benchmark under CodSpeed's simulation: more than hashing the data itself. std::io::copy reads through a stack buffer that it does not zero for readers able to fill uninitialized memory, such as files and byte slices. Feed the digest (and the time macro finder) through it as a Write, so that nothing is zeroed and nothing stays allocated between calls. Local medians (divan), main / per-thread buffer / 16KB stack buffer / this: hash_multiple_files 35.6 / 20.2 / 20.5 / 19.6 us hash_header_file 7.4 / 5.9 / 5.9 / 5.7 us hash_large_data 867 / 829 / 875 / 836 us time_macro_finder_no_macros 300 / 274 / 299 / 268 us
1 parent 62a4307 commit eca2f55

1 file changed

Lines changed: 23 additions & 52 deletions

File tree

‎src/util.rs‎

Lines changed: 23 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -39,56 +39,29 @@ pub const BASE64_URL_SAFE_ENGINE: base64::engine::GeneralPurpose =
3939

4040
pub const HASH_BUFFER_SIZE: usize = 128 * 1024;
4141

42-
/// Size of the buffer used for the first reads when hashing. Zeroing a full
43-
/// `HASH_BUFFER_SIZE` buffer costs more than hashing a typical header, which
44-
/// is what direct mode hashes the most.
45-
const SMALL_HASH_BUFFER_SIZE: usize = 16 * 1024;
46-
47-
/// Amount of data read with the small buffer before switching to a
48-
/// `HASH_BUFFER_SIZE` one.
49-
const SMALL_HASH_BUFFER_LIMIT: usize = 64 * 1024;
50-
5142
/// Reads `reader` to the end, passing each chunk read to `f`.
5243
///
53-
/// The first `SMALL_HASH_BUFFER_LIMIT` bytes are read with a small buffer,
54-
/// and only larger inputs switch to a `HASH_BUFFER_SIZE` one. Both buffers
55-
/// live on the stack, so nothing stays allocated between calls.
56-
fn read_chunks<R: Read + ?Sized>(reader: &mut R, mut f: impl FnMut(&[u8])) -> Result<()> {
57-
if read_chunks_small(reader, &mut f)? {
58-
read_chunks_large(reader, &mut f)?;
59-
}
60-
Ok(())
61-
}
62-
63-
/// Returns whether there may be more to read.
64-
// Not inlined, so that both buffers are not on the stack at the same time.
65-
#[inline(never)]
66-
fn read_chunks_small<R: Read + ?Sized>(reader: &mut R, f: &mut impl FnMut(&[u8])) -> Result<bool> {
67-
let mut buffer = [0u8; SMALL_HASH_BUFFER_SIZE];
68-
let mut total = 0;
69-
while total < SMALL_HASH_BUFFER_LIMIT {
70-
let count = reader.read(&mut buffer)?;
71-
if count == 0 {
72-
return Ok(false);
44+
/// Goes through `std::io::copy`, whose buffer lives on the stack and is not
45+
/// zeroed for readers that can fill uninitialized memory, such as files and
46+
/// byte slices. Zeroing a buffer for each input costs more than hashing a
47+
/// typical header, which is what direct mode hashes the most, and keeping one
48+
/// around between calls keeps that memory allocated.
49+
fn read_chunks<R: Read + ?Sized>(reader: &mut R, f: impl FnMut(&[u8])) -> Result<()> {
50+
struct Chunks<F>(F);
51+
52+
impl<F: FnMut(&[u8])> Write for Chunks<F> {
53+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
54+
(self.0)(buf);
55+
Ok(buf.len())
7356
}
74-
f(&buffer[..count]);
75-
total += count;
76-
}
77-
Ok(true)
78-
}
7957

80-
#[inline(never)]
81-
fn read_chunks_large<R: Read + ?Sized>(reader: &mut R, f: &mut impl FnMut(&[u8])) -> Result<()> {
82-
// A buffer of 128KB should give us the best performance.
83-
// See https://eklitzke.org/efficient-file-copying-on-linux.
84-
let mut buffer = [0u8; HASH_BUFFER_SIZE];
85-
loop {
86-
let count = reader.read(&mut buffer)?;
87-
if count == 0 {
88-
return Ok(());
58+
fn flush(&mut self) -> std::io::Result<()> {
59+
Ok(())
8960
}
90-
f(&buffer[..count]);
9161
}
62+
63+
std::io::copy(reader, &mut Chunks(f))?;
64+
Ok(())
9265
}
9366

9467
#[derive(Clone)]
@@ -1676,14 +1649,12 @@ mod tests {
16761649

16771650
#[test]
16781651
fn test_reader_sync_across_buffer_sizes() {
1679-
use super::{HASH_BUFFER_SIZE, SMALL_HASH_BUFFER_LIMIT, SMALL_HASH_BUFFER_SIZE};
1680-
// Sizes around the end of the first small chunk, the switch from the
1681-
// small buffer to the large one, and the end of the first large chunk.
1682-
let boundaries = [
1683-
SMALL_HASH_BUFFER_SIZE,
1684-
SMALL_HASH_BUFFER_LIMIT,
1685-
SMALL_HASH_BUFFER_LIMIT + HASH_BUFFER_SIZE,
1686-
];
1652+
use super::HASH_BUFFER_SIZE;
1653+
// `std::io::copy` reads 8KB at a time (std's `DEFAULT_BUF_SIZE`):
1654+
// sizes around the end of the first and second chunks, and around
1655+
// `HASH_BUFFER_SIZE`, the chunk size used before.
1656+
const COPY_CHUNK: usize = 8 * 1024;
1657+
let boundaries = [COPY_CHUNK, 2 * COPY_CHUNK, HASH_BUFFER_SIZE];
16871658
for boundary in boundaries {
16881659
for len in [boundary - 1, boundary, boundary + 1] {
16891660
let data: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();

0 commit comments

Comments
 (0)