]>
Commit | Line | Data |
---|---|---|
abe05a73 XL |
1 | //! This is a copy of `core::hash::sip` adapted to providing 128 bit hashes. |
2 | ||
abe05a73 | 3 | use std::hash::Hasher; |
29967ef6 | 4 | use std::mem::{self, MaybeUninit}; |
dfeec247 | 5 | use std::ptr; |
abe05a73 | 6 | |
416331ca XL |
7 | #[cfg(test)] |
8 | mod tests; | |
9 | ||
29967ef6 XL |
10 | // The SipHash algorithm operates on 8-byte chunks. |
11 | const ELEM_SIZE: usize = mem::size_of::<u64>(); | |
12 | ||
13 | // Size of the buffer in number of elements, not including the spill. | |
14 | // | |
15 | // The selection of this size was guided by rustc-perf benchmark comparisons of | |
16 | // different buffer sizes. It should be periodically reevaluated as the compiler | |
17 | // implementation and input characteristics change. | |
18 | // | |
19 | // Using the same-sized buffer for everything we hash is a performance versus | |
20 | // complexity tradeoff. The ideal buffer size, and whether buffering should even | |
21 | // be used, depends on what is being hashed. It may be worth it to size the | |
22 | // buffer appropriately (perhaps by making SipHasher128 generic over the buffer | |
23 | // size) or disable buffering depending on what is being hashed. But at this | |
24 | // time, we use the same buffer size for everything. | |
25 | const BUFFER_CAPACITY: usize = 8; | |
26 | ||
27 | // Size of the buffer in bytes, not including the spill. | |
28 | const BUFFER_SIZE: usize = BUFFER_CAPACITY * ELEM_SIZE; | |
29 | ||
30 | // Size of the buffer in number of elements, including the spill. | |
31 | const BUFFER_WITH_SPILL_CAPACITY: usize = BUFFER_CAPACITY + 1; | |
32 | ||
33 | // Size of the buffer in bytes, including the spill. | |
34 | const BUFFER_WITH_SPILL_SIZE: usize = BUFFER_WITH_SPILL_CAPACITY * ELEM_SIZE; | |
35 | ||
36 | // Index of the spill element in the buffer. | |
37 | const BUFFER_SPILL_INDEX: usize = BUFFER_WITH_SPILL_CAPACITY - 1; | |
38 | ||
abe05a73 | 39 | #[derive(Debug, Clone)] |
29967ef6 | 40 | #[repr(C)] |
abe05a73 | 41 | pub struct SipHasher128 { |
29967ef6 XL |
42 | // The access pattern during hashing consists of accesses to `nbuf` and |
43 | // `buf` until the buffer is full, followed by accesses to `state` and | |
44 | // `processed`, and then repetition of that pattern until hashing is done. | |
45 | // This is the basis for the ordering of fields below. However, in practice | |
46 | // the cache miss-rate for data access is extremely low regardless of order. | |
47 | nbuf: usize, // how many bytes in buf are valid | |
48 | buf: [MaybeUninit<u64>; BUFFER_WITH_SPILL_CAPACITY], // unprocessed bytes le | |
49 | state: State, // hash State | |
50 | processed: usize, // how many bytes we've processed | |
abe05a73 XL |
51 | } |
52 | ||
53 | #[derive(Debug, Clone, Copy)] | |
54 | #[repr(C)] | |
55 | struct State { | |
56 | // v0, v2 and v1, v3 show up in pairs in the algorithm, | |
57 | // and simd implementations of SipHash will use vectors | |
58 | // of v02 and v13. By placing them in this order in the struct, | |
59 | // the compiler can pick up on just a few simd optimizations by itself. | |
60 | v0: u64, | |
61 | v2: u64, | |
62 | v1: u64, | |
63 | v3: u64, | |
64 | } | |
65 | ||
66 | macro_rules! compress { | |
dfeec247 XL |
67 | ($state:expr) => {{ compress!($state.v0, $state.v1, $state.v2, $state.v3) }}; |
68 | ($v0:expr, $v1:expr, $v2:expr, $v3:expr) => {{ | |
69 | $v0 = $v0.wrapping_add($v1); | |
70 | $v1 = $v1.rotate_left(13); | |
71 | $v1 ^= $v0; | |
abe05a73 | 72 | $v0 = $v0.rotate_left(32); |
dfeec247 XL |
73 | $v2 = $v2.wrapping_add($v3); |
74 | $v3 = $v3.rotate_left(16); | |
75 | $v3 ^= $v2; | |
76 | $v0 = $v0.wrapping_add($v3); | |
77 | $v3 = $v3.rotate_left(21); | |
78 | $v3 ^= $v0; | |
79 | $v2 = $v2.wrapping_add($v1); | |
80 | $v1 = $v1.rotate_left(17); | |
81 | $v1 ^= $v2; | |
abe05a73 | 82 | $v2 = $v2.rotate_left(32); |
dfeec247 | 83 | }}; |
abe05a73 XL |
84 | } |
85 | ||
29967ef6 XL |
86 | // Copies up to 8 bytes from source to destination. This performs better than |
87 | // `ptr::copy_nonoverlapping` on microbenchmarks and may perform better on real | |
88 | // workloads since all of the copies have fixed sizes and avoid calling memcpy. | |
89 | // | |
90 | // This is specifically designed for copies of up to 8 bytes, because that's the | |
91 | // maximum of number bytes needed to fill an 8-byte-sized element on which | |
92 | // SipHash operates. Note that for variable-sized copies which are known to be | |
93 | // less than 8 bytes, this function will perform more work than necessary unless | |
94 | // the compiler is able to optimize the extra work away. | |
abe05a73 | 95 | #[inline] |
29967ef6 XL |
96 | unsafe fn copy_nonoverlapping_small(src: *const u8, dst: *mut u8, count: usize) { |
97 | debug_assert!(count <= 8); | |
98 | ||
99 | if count == 8 { | |
100 | ptr::copy_nonoverlapping(src, dst, 8); | |
101 | return; | |
102 | } | |
103 | ||
104 | let mut i = 0; | |
105 | if i + 3 < count { | |
106 | ptr::copy_nonoverlapping(src.add(i), dst.add(i), 4); | |
abe05a73 XL |
107 | i += 4; |
108 | } | |
29967ef6 XL |
109 | |
110 | if i + 1 < count { | |
111 | ptr::copy_nonoverlapping(src.add(i), dst.add(i), 2); | |
abe05a73 XL |
112 | i += 2 |
113 | } | |
29967ef6 XL |
114 | |
115 | if i < count { | |
116 | *dst.add(i) = *src.add(i); | |
abe05a73 XL |
117 | i += 1; |
118 | } | |
29967ef6 XL |
119 | |
120 | debug_assert_eq!(i, count); | |
abe05a73 XL |
121 | } |
122 | ||
29967ef6 XL |
123 | // # Implementation |
124 | // | |
125 | // This implementation uses buffering to reduce the hashing cost for inputs | |
126 | // consisting of many small integers. Buffering simplifies the integration of | |
127 | // integer input--the integer write function typically just appends to the | |
128 | // buffer with a statically sized write, updates metadata, and returns. | |
129 | // | |
130 | // Buffering also prevents alternating between writes that do and do not trigger | |
131 | // the hashing process. Only when the entire buffer is full do we transition | |
132 | // into hashing. This allows us to keep the hash state in registers for longer, | |
133 | // instead of loading and storing it before and after processing each element. | |
134 | // | |
135 | // When a write fills the buffer, a buffer processing function is invoked to | |
136 | // hash all of the buffered input. The buffer processing functions are marked | |
137 | // `#[inline(never)]` so that they aren't inlined into the append functions, | |
138 | // which ensures the more frequently called append functions remain inlineable | |
139 | // and don't include register pushing/popping that would only be made necessary | |
140 | // by inclusion of the complex buffer processing path which uses those | |
141 | // registers. | |
142 | // | |
143 | // The buffer includes a "spill"--an extra element at the end--which simplifies | |
144 | // the integer write buffer processing path. The value that fills the buffer can | |
145 | // be written with a statically sized write that may spill over into the spill. | |
146 | // After the buffer is processed, the part of the value that spilled over can be | |
147 | // written from the spill to the beginning of the buffer with another statically | |
148 | // sized write. This write may copy more bytes than actually spilled over, but | |
149 | // we maintain the metadata such that any extra copied bytes will be ignored by | |
150 | // subsequent processing. Due to the static sizes, this scheme performs better | |
151 | // than copying the exact number of bytes needed into the end and beginning of | |
152 | // the buffer. | |
153 | // | |
154 | // The buffer is uninitialized, which improves performance, but may preclude | |
155 | // efficient implementation of alternative approaches. The improvement is not so | |
156 | // large that an alternative approach should be disregarded because it cannot be | |
157 | // efficiently implemented with an uninitialized buffer. On the other hand, an | |
158 | // uninitialized buffer may become more important should a larger one be used. | |
159 | // | |
160 | // # Platform Dependence | |
161 | // | |
162 | // The SipHash algorithm operates on byte sequences. It parses the input stream | |
163 | // as 8-byte little-endian integers. Therefore, given the same byte sequence, it | |
164 | // produces the same result on big- and little-endian hardware. | |
165 | // | |
166 | // However, the Hasher trait has methods which operate on multi-byte integers. | |
167 | // How they are converted into byte sequences can be endian-dependent (by using | |
168 | // native byte order) or independent (by consistently using either LE or BE byte | |
169 | // order). It can also be `isize` and `usize` size dependent (by using the | |
170 | // native size), or independent (by converting to a common size), supposing the | |
171 | // values can be represented in 32 bits. | |
172 | // | |
173 | // In order to make `SipHasher128` consistent with `SipHasher` in libstd, we | |
174 | // choose to do the integer to byte sequence conversion in the platform- | |
175 | // dependent way. Clients can achieve platform-independent hashing by widening | |
176 | // `isize` and `usize` integers to 64 bits on 32-bit systems and byte-swapping | |
177 | // integers on big-endian systems before passing them to the writing functions. | |
178 | // This causes the input byte sequence to look identical on big- and little- | |
179 | // endian systems (supposing `isize` and `usize` values can be represented in 32 | |
180 | // bits), which ensures platform-independent results. | |
abe05a73 XL |
181 | impl SipHasher128 { |
182 | #[inline] | |
183 | pub fn new_with_keys(key0: u64, key1: u64) -> SipHasher128 { | |
29967ef6 XL |
184 | let mut hasher = SipHasher128 { |
185 | nbuf: 0, | |
186 | buf: MaybeUninit::uninit_array(), | |
187 | state: State { | |
188 | v0: key0 ^ 0x736f6d6570736575, | |
189 | // The XOR with 0xee is only done on 128-bit algorithm version. | |
190 | v1: key1 ^ (0x646f72616e646f6d ^ 0xee), | |
191 | v2: key0 ^ 0x6c7967656e657261, | |
192 | v3: key1 ^ 0x7465646279746573, | |
193 | }, | |
194 | processed: 0, | |
abe05a73 | 195 | }; |
29967ef6 XL |
196 | |
197 | unsafe { | |
198 | // Initialize spill because we read from it in `short_write_process_buffer`. | |
199 | *hasher.buf.get_unchecked_mut(BUFFER_SPILL_INDEX) = MaybeUninit::zeroed(); | |
200 | } | |
201 | ||
202 | hasher | |
abe05a73 XL |
203 | } |
204 | ||
205 | #[inline] | |
5099ac24 | 206 | pub fn short_write<const LEN: usize>(&mut self, bytes: [u8; LEN]) { |
29967ef6 | 207 | let nbuf = self.nbuf; |
5099ac24 | 208 | debug_assert!(LEN <= 8); |
29967ef6 | 209 | debug_assert!(nbuf < BUFFER_SIZE); |
5099ac24 | 210 | debug_assert!(nbuf + LEN < BUFFER_WITH_SPILL_SIZE); |
29967ef6 | 211 | |
5099ac24 | 212 | if nbuf + LEN < BUFFER_SIZE { |
29967ef6 XL |
213 | unsafe { |
214 | // The memcpy call is optimized away because the size is known. | |
215 | let dst = (self.buf.as_mut_ptr() as *mut u8).add(nbuf); | |
5099ac24 | 216 | ptr::copy_nonoverlapping(bytes.as_ptr(), dst, LEN); |
29967ef6 XL |
217 | } |
218 | ||
5099ac24 | 219 | self.nbuf = nbuf + LEN; |
29967ef6 XL |
220 | |
221 | return; | |
222 | } | |
223 | ||
5099ac24 | 224 | unsafe { self.short_write_process_buffer(bytes) } |
abe05a73 XL |
225 | } |
226 | ||
29967ef6 XL |
227 | // A specialized write function for values with size <= 8 that should only |
228 | // be called when the write would cause the buffer to fill. | |
1b1a35ee | 229 | // |
29967ef6 XL |
230 | // SAFETY: the write of `x` into `self.buf` starting at byte offset |
231 | // `self.nbuf` must cause `self.buf` to become fully initialized (and not | |
232 | // overflow) if it wasn't already. | |
233 | #[inline(never)] | |
5099ac24 | 234 | unsafe fn short_write_process_buffer<const LEN: usize>(&mut self, bytes: [u8; LEN]) { |
29967ef6 | 235 | let nbuf = self.nbuf; |
5099ac24 | 236 | debug_assert!(LEN <= 8); |
29967ef6 | 237 | debug_assert!(nbuf < BUFFER_SIZE); |
5099ac24 FG |
238 | debug_assert!(nbuf + LEN >= BUFFER_SIZE); |
239 | debug_assert!(nbuf + LEN < BUFFER_WITH_SPILL_SIZE); | |
29967ef6 XL |
240 | |
241 | // Copy first part of input into end of buffer, possibly into spill | |
242 | // element. The memcpy call is optimized away because the size is known. | |
243 | let dst = (self.buf.as_mut_ptr() as *mut u8).add(nbuf); | |
5099ac24 | 244 | ptr::copy_nonoverlapping(bytes.as_ptr(), dst, LEN); |
29967ef6 XL |
245 | |
246 | // Process buffer. | |
247 | for i in 0..BUFFER_CAPACITY { | |
248 | let elem = self.buf.get_unchecked(i).assume_init().to_le(); | |
249 | self.state.v3 ^= elem; | |
250 | Sip24Rounds::c_rounds(&mut self.state); | |
251 | self.state.v0 ^= elem; | |
252 | } | |
253 | ||
5099ac24 FG |
254 | // Copy remaining input into start of buffer by copying LEN - 1 |
255 | // elements from spill (at most LEN - 1 bytes could have overflowed | |
29967ef6 | 256 | // into the spill). The memcpy call is optimized away because the size |
5099ac24 | 257 | // is known. And the whole copy is optimized away for LEN == 1. |
29967ef6 | 258 | let src = self.buf.get_unchecked(BUFFER_SPILL_INDEX) as *const _ as *const u8; |
5099ac24 | 259 | ptr::copy_nonoverlapping(src, self.buf.as_mut_ptr() as *mut u8, LEN - 1); |
29967ef6 XL |
260 | |
261 | // This function should only be called when the write fills the buffer. | |
5099ac24 FG |
262 | // Therefore, when LEN == 1, the new `self.nbuf` must be zero. |
263 | // LEN is statically known, so the branch is optimized away. | |
264 | self.nbuf = if LEN == 1 { 0 } else { nbuf + LEN - BUFFER_SIZE }; | |
29967ef6 XL |
265 | self.processed += BUFFER_SIZE; |
266 | } | |
267 | ||
268 | // A write function for byte slices. | |
269 | #[inline] | |
270 | fn slice_write(&mut self, msg: &[u8]) { | |
271 | let length = msg.len(); | |
272 | let nbuf = self.nbuf; | |
273 | debug_assert!(nbuf < BUFFER_SIZE); | |
274 | ||
275 | if nbuf + length < BUFFER_SIZE { | |
276 | unsafe { | |
277 | let dst = (self.buf.as_mut_ptr() as *mut u8).add(nbuf); | |
278 | ||
279 | if length <= 8 { | |
280 | copy_nonoverlapping_small(msg.as_ptr(), dst, length); | |
281 | } else { | |
282 | // This memcpy is *not* optimized away. | |
283 | ptr::copy_nonoverlapping(msg.as_ptr(), dst, length); | |
284 | } | |
285 | } | |
286 | ||
287 | self.nbuf = nbuf + length; | |
288 | ||
74b04a01 | 289 | return; |
abe05a73 | 290 | } |
74b04a01 | 291 | |
29967ef6 XL |
292 | unsafe { self.slice_write_process_buffer(msg) } |
293 | } | |
294 | ||
295 | // A write function for byte slices that should only be called when the | |
296 | // write would cause the buffer to fill. | |
297 | // | |
298 | // SAFETY: `self.buf` must be initialized up to the byte offset `self.nbuf`, | |
299 | // and `msg` must contain enough bytes to initialize the rest of the element | |
300 | // containing the byte offset `self.nbuf`. | |
301 | #[inline(never)] | |
302 | unsafe fn slice_write_process_buffer(&mut self, msg: &[u8]) { | |
303 | let length = msg.len(); | |
304 | let nbuf = self.nbuf; | |
305 | debug_assert!(nbuf < BUFFER_SIZE); | |
306 | debug_assert!(nbuf + length >= BUFFER_SIZE); | |
307 | ||
308 | // Always copy first part of input into current element of buffer. | |
309 | // This function should only be called when the write fills the buffer, | |
310 | // so we know that there is enough input to fill the current element. | |
311 | let valid_in_elem = nbuf % ELEM_SIZE; | |
312 | let needed_in_elem = ELEM_SIZE - valid_in_elem; | |
313 | ||
314 | let src = msg.as_ptr(); | |
315 | let dst = (self.buf.as_mut_ptr() as *mut u8).add(nbuf); | |
316 | copy_nonoverlapping_small(src, dst, needed_in_elem); | |
317 | ||
318 | // Process buffer. | |
319 | ||
320 | // Using `nbuf / ELEM_SIZE + 1` rather than `(nbuf + needed_in_elem) / | |
321 | // ELEM_SIZE` to show the compiler that this loop's upper bound is > 0. | |
322 | // We know that is true, because last step ensured we have a full | |
323 | // element in the buffer. | |
324 | let last = nbuf / ELEM_SIZE + 1; | |
325 | ||
326 | for i in 0..last { | |
327 | let elem = self.buf.get_unchecked(i).assume_init().to_le(); | |
328 | self.state.v3 ^= elem; | |
329 | Sip24Rounds::c_rounds(&mut self.state); | |
330 | self.state.v0 ^= elem; | |
331 | } | |
332 | ||
333 | // Process the remaining element-sized chunks of input. | |
334 | let mut processed = needed_in_elem; | |
335 | let input_left = length - processed; | |
336 | let elems_left = input_left / ELEM_SIZE; | |
337 | let extra_bytes_left = input_left % ELEM_SIZE; | |
338 | ||
339 | for _ in 0..elems_left { | |
340 | let elem = (msg.as_ptr().add(processed) as *const u64).read_unaligned().to_le(); | |
341 | self.state.v3 ^= elem; | |
342 | Sip24Rounds::c_rounds(&mut self.state); | |
343 | self.state.v0 ^= elem; | |
344 | processed += ELEM_SIZE; | |
345 | } | |
346 | ||
347 | // Copy remaining input into start of buffer. | |
348 | let src = msg.as_ptr().add(processed); | |
349 | let dst = self.buf.as_mut_ptr() as *mut u8; | |
350 | copy_nonoverlapping_small(src, dst, extra_bytes_left); | |
351 | ||
352 | self.nbuf = extra_bytes_left; | |
353 | self.processed += nbuf + processed; | |
abe05a73 XL |
354 | } |
355 | ||
356 | #[inline] | |
357 | pub fn finish128(mut self) -> (u64, u64) { | |
29967ef6 | 358 | debug_assert!(self.nbuf < BUFFER_SIZE); |
abe05a73 | 359 | |
29967ef6 XL |
360 | // Process full elements in buffer. |
361 | let last = self.nbuf / ELEM_SIZE; | |
abe05a73 | 362 | |
29967ef6 XL |
363 | // Since we're consuming self, avoid updating members for a potential |
364 | // performance gain. | |
365 | let mut state = self.state; | |
366 | ||
367 | for i in 0..last { | |
368 | let elem = unsafe { self.buf.get_unchecked(i).assume_init().to_le() }; | |
369 | state.v3 ^= elem; | |
370 | Sip24Rounds::c_rounds(&mut state); | |
371 | state.v0 ^= elem; | |
372 | } | |
373 | ||
374 | // Get remaining partial element. | |
375 | let elem = if self.nbuf % ELEM_SIZE != 0 { | |
376 | unsafe { | |
377 | // Ensure element is initialized by writing zero bytes. At most | |
378 | // `ELEM_SIZE - 1` are required given the above check. It's safe | |
379 | // to write this many because we have the spill and we maintain | |
380 | // `self.nbuf` such that this write will start before the spill. | |
381 | let dst = (self.buf.as_mut_ptr() as *mut u8).add(self.nbuf); | |
382 | ptr::write_bytes(dst, 0, ELEM_SIZE - 1); | |
383 | self.buf.get_unchecked(last).assume_init().to_le() | |
384 | } | |
385 | } else { | |
386 | 0 | |
387 | }; | |
388 | ||
389 | // Finalize the hash. | |
390 | let length = self.processed + self.nbuf; | |
391 | let b: u64 = ((length as u64 & 0xff) << 56) | elem; | |
392 | ||
393 | state.v3 ^= b; | |
394 | Sip24Rounds::c_rounds(&mut state); | |
395 | state.v0 ^= b; | |
396 | ||
397 | state.v2 ^= 0xee; | |
398 | Sip24Rounds::d_rounds(&mut state); | |
399 | let _0 = state.v0 ^ state.v1 ^ state.v2 ^ state.v3; | |
400 | ||
401 | state.v1 ^= 0xdd; | |
402 | Sip24Rounds::d_rounds(&mut state); | |
403 | let _1 = state.v0 ^ state.v1 ^ state.v2 ^ state.v3; | |
abe05a73 | 404 | |
abe05a73 XL |
405 | (_0, _1) |
406 | } | |
407 | } | |
408 | ||
409 | impl Hasher for SipHasher128 { | |
410 | #[inline] | |
411 | fn write_u8(&mut self, i: u8) { | |
5099ac24 | 412 | self.short_write(i.to_ne_bytes()); |
abe05a73 XL |
413 | } |
414 | ||
415 | #[inline] | |
416 | fn write_u16(&mut self, i: u16) { | |
5099ac24 | 417 | self.short_write(i.to_ne_bytes()); |
abe05a73 XL |
418 | } |
419 | ||
420 | #[inline] | |
421 | fn write_u32(&mut self, i: u32) { | |
5099ac24 | 422 | self.short_write(i.to_ne_bytes()); |
abe05a73 XL |
423 | } |
424 | ||
425 | #[inline] | |
426 | fn write_u64(&mut self, i: u64) { | |
5099ac24 | 427 | self.short_write(i.to_ne_bytes()); |
abe05a73 XL |
428 | } |
429 | ||
430 | #[inline] | |
431 | fn write_usize(&mut self, i: usize) { | |
5099ac24 | 432 | self.short_write(i.to_ne_bytes()); |
abe05a73 XL |
433 | } |
434 | ||
435 | #[inline] | |
436 | fn write_i8(&mut self, i: i8) { | |
5099ac24 | 437 | self.short_write((i as u8).to_ne_bytes()); |
abe05a73 XL |
438 | } |
439 | ||
440 | #[inline] | |
441 | fn write_i16(&mut self, i: i16) { | |
5099ac24 | 442 | self.short_write((i as u16).to_ne_bytes()); |
abe05a73 XL |
443 | } |
444 | ||
445 | #[inline] | |
446 | fn write_i32(&mut self, i: i32) { | |
5099ac24 | 447 | self.short_write((i as u32).to_ne_bytes()); |
abe05a73 XL |
448 | } |
449 | ||
450 | #[inline] | |
451 | fn write_i64(&mut self, i: i64) { | |
5099ac24 | 452 | self.short_write((i as u64).to_ne_bytes()); |
abe05a73 XL |
453 | } |
454 | ||
455 | #[inline] | |
456 | fn write_isize(&mut self, i: isize) { | |
5099ac24 | 457 | self.short_write((i as usize).to_ne_bytes()); |
abe05a73 XL |
458 | } |
459 | ||
460 | #[inline] | |
461 | fn write(&mut self, msg: &[u8]) { | |
29967ef6 | 462 | self.slice_write(msg); |
abe05a73 XL |
463 | } |
464 | ||
465 | fn finish(&self) -> u64 { | |
466 | panic!("SipHasher128 cannot provide valid 64 bit hashes") | |
467 | } | |
468 | } | |
469 | ||
470 | #[derive(Debug, Clone, Default)] | |
471 | struct Sip24Rounds; | |
472 | ||
473 | impl Sip24Rounds { | |
474 | #[inline] | |
475 | fn c_rounds(state: &mut State) { | |
476 | compress!(state); | |
477 | compress!(state); | |
478 | } | |
479 | ||
480 | #[inline] | |
481 | fn d_rounds(state: &mut State) { | |
482 | compress!(state); | |
483 | compress!(state); | |
484 | compress!(state); | |
485 | compress!(state); | |
486 | } | |
487 | } |