Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make StableHasher::finish return a small hash #6

Merged
merged 3 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Unreleased

- `StableHasher::finish` now returns a small hash instead of being fatal (#6)
- Remove `StableHasher::finalize` (#4)
- Import stable hasher implementation from rustc ([db8aca48129](https://github.com/rust-lang/rust/blob/db8aca48129d86b2623e3ac8cbcf2902d4d313ad/compiler/rustc_data_structures/src/))
36 changes: 23 additions & 13 deletions src/sip128.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,41 +377,47 @@ impl SipHasher128 {
}
}

#[inline]
#[inline(always)]
pub fn finish128(mut self) -> [u64; 2] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an idea if this affects performance?

Copy link
Member Author

@Urgau Urgau Jul 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted finish128 method on Godbolt and look at the diff with the previous version and there was significant increase in "predicted cycles" and instructions, which was due to the ptr::write_bytes call generating a memcpy instead of a small instructions because the size wasn't a constant anymore.

I therefore changed that part (Godbolt) to copy the "last" and "last + 1" in a array as to have the size a constant as before and now the diff is very small, llvm-mca indicates 15100ins -> 15400ins for 100 iterations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for investigating!

Since this code is super hot in the compiler and the u64 version of the finish method probably isn't going to be used much, I'd prefer if we didn't regress things at all.

How about extracting a finish128_inner(nbuf: usize, buf: &mut [MaybeUninit<u64>; BUFFER_WITH_SPILL_CAPACITY], state: State, processed: usize) function? Then Hasher::finish could would only need to copy state and finish128 could keep taking self by value (?_

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at the generated assembly on Godbolt and it's very very similar, mainly some registry naming changes, some instructions moving place and one more stack variable.

Compared to the other version (with the last and last+1 copy), it's roughly similar, in terms of instructions per llvm-mca.

One interesting thing, with the inner variant the number of cycles needed per llvm-mca is dropping significantly from 4410 to 3810, maybe because of less memory pressure. That seems like a win.

Pushed the inner variant.

debug_assert!(self.nbuf < BUFFER_SIZE);
SipHasher128::finish128_inner(self.nbuf, &mut self.buf, self.state, self.processed)
}

// Process full elements in buffer.
let last = self.nbuf / ELEM_SIZE;
#[inline]
fn finish128_inner(
nbuf: usize,
buf: &mut [MaybeUninit<u64>; BUFFER_WITH_SPILL_CAPACITY],
mut state: State,
processed: usize,
) -> [u64; 2] {
debug_assert!(nbuf < BUFFER_SIZE);

// Since we're consuming self, avoid updating members for a potential
// performance gain.
let mut state = self.state;
// Process full elements in buffer.
let last = nbuf / ELEM_SIZE;

for i in 0..last {
let elem = unsafe { self.buf.get_unchecked(i).assume_init().to_le() };
let elem = unsafe { buf.get_unchecked(i).assume_init().to_le() };
state.v3 ^= elem;
Sip13Rounds::c_rounds(&mut state);
state.v0 ^= elem;
}

// Get remaining partial element.
let elem = if self.nbuf % ELEM_SIZE != 0 {
let elem = if nbuf % ELEM_SIZE != 0 {
unsafe {
// Ensure element is initialized by writing zero bytes. At most
// `ELEM_SIZE - 1` are required given the above check. It's safe
// to write this many because we have the spill and we maintain
// `self.nbuf` such that this write will start before the spill.
let dst = (self.buf.as_mut_ptr() as *mut u8).add(self.nbuf);
let dst = (buf.as_mut_ptr() as *mut u8).add(nbuf);
ptr::write_bytes(dst, 0, ELEM_SIZE - 1);
self.buf.get_unchecked(last).assume_init().to_le()
buf.get_unchecked(last).assume_init().to_le()
}
} else {
0
};

// Finalize the hash.
let length = self.processed.debug_strict_add(self.nbuf);
let length = processed.debug_strict_add(nbuf);
let b: u64 = ((length as u64 & 0xff) << 56) | elem;

state.v3 ^= b;
Expand Down Expand Up @@ -496,7 +502,11 @@ impl Hasher for SipHasher128 {
}

fn finish(&self) -> u64 {
panic!("SipHasher128 cannot provide valid 64 bit hashes")
let mut buf = self.buf.clone();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we still need to clone the buffer 🤷
Hasher::finish is just defined in an unfortunate way.

let [a, b] = SipHasher128::finish128_inner(self.nbuf, &mut buf, self.state, self.processed);

// Combining the two halves makes sure we get a good quality hash.
a.wrapping_mul(3).wrapping_add(b).to_le()
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/sip128/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,13 @@ fn test_fill_buffer() {
test_fill_buffer!(i128, write_i128);
test_fill_buffer!(isize, write_isize);
}

#[test]
fn test_finish() {
let mut hasher = SipHasher128::new_with_keys(0, 0);

hasher.write_isize(0xF0);
hasher.write_isize(0xF0010);

assert_eq!(hasher.finish(), hasher.finish());
}
11 changes: 3 additions & 8 deletions src/stable_hasher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,11 @@ impl fmt::Debug for StableHasher {
}

impl Hasher for StableHasher {
/// <div class="warning">
/// Returns a combined hash.
///
/// Do not use this function, it will unconditionnaly panic.
///
/// Use instead [`StableHasher::finish`] which returns a
/// `[u64; 2]` for greater precision.
///
/// </div>
/// For greater precision use instead [`StableHasher::finish`].
fn finish(&self) -> u64 {
panic!("use StableHasher::finalize instead");
self.state.finish()
}

#[inline]
Expand Down