Skip to main content

yo_common/
eq.rs

1//! Comparing two byte strings without leaving the function.
2//!
3//! `a == b` on two slices ends up in the platform's `memcmp`. That is the right
4//! answer for a megabyte and the wrong one for a key: a profile of `SADD` on
5//! the wire had seven percent of the command inside `_platform_memcmp` and the
6//! stub that reaches it, comparing nineteen bytes. The call itself, the length
7//! dispatch inside it and the return are most of that, and none of it is the
8//! comparison.
9//!
10//! So this compares in machine words and stays inline. A key or a member is
11//! almost always shorter than a cache line, and the shapes that matter are the
12//! ones a benchmark and a real workload agree on: `key:000000000001` at
13//! fourteen bytes, `member:000000000001` at nineteen, a session id at thirty
14//! two.
15//!
16//! # The last word overlaps
17//!
18//! A comparison of nineteen bytes reads bytes 0..8, 8..16 and then 11..19,
19//! which covers the whole string with three loads and no tail loop. The middle
20//! five bytes are read twice, which costs nothing and is what keeps the shape
21//! branch free. Below eight bytes the same trick runs on four byte words, and
22//! below four it is a loop of at most three bytes, which is shorter than any
23//! cleverness would be.
24//!
25//! # This is equality and not ordering
26//!
27//! There is no `cmp` here on purpose. Nothing on the hot path needs to know
28//! which of two keys sorts first, and a word wise ordering would have to
29//! byte swap on a little endian machine to get the answer right, which is the
30//! sort of subtlety that is worth avoiding when nothing is asking for it.
31
32/// The eight bytes at `at`, as a machine word.
33#[inline(always)]
34fn w8(s: &[u8], at: usize) -> u64 {
35    u64::from_ne_bytes([
36        s[at],
37        s[at + 1],
38        s[at + 2],
39        s[at + 3],
40        s[at + 4],
41        s[at + 5],
42        s[at + 6],
43        s[at + 7],
44    ])
45}
46
47/// The four bytes at `at`, as a half word.
48#[inline(always)]
49fn w4(s: &[u8], at: usize) -> u32 {
50    u32::from_ne_bytes([s[at], s[at + 1], s[at + 2], s[at + 3]])
51}
52
53/// Whether `a` and `b` hold the same bytes.
54///
55/// The same answer as `a == b` and the same cost model as `memcmp` for a long
56/// string, without the call for a short one.
57#[inline]
58#[must_use]
59pub fn bytes_eq(a: &[u8], b: &[u8]) -> bool {
60    let n = a.len();
61    if n != b.len() {
62        return false;
63    }
64    if n >= 8 {
65        let mut at = 0;
66        while at + 8 < n {
67            if w8(a, at) != w8(b, at) {
68                return false;
69            }
70            at += 8;
71        }
72        // The last whole word, which reaches back over bytes already compared
73        // when the length is not a multiple of eight.
74        return w8(a, n - 8) == w8(b, n - 8);
75    }
76    if n >= 4 {
77        return w4(a, 0) == w4(b, 0) && w4(a, n - 4) == w4(b, n - 4);
78    }
79    let mut at = 0;
80    while at < n {
81        if a[at] != b[at] {
82            return false;
83        }
84        at += 1;
85    }
86    true
87}
88
89#[cfg(test)]
90mod tests {
91    use super::bytes_eq;
92
93    /// Every length from nothing to past two words, equal and then not equal at
94    /// every position in turn.
95    ///
96    /// The overlapping last word is the part worth being thorough about: a
97    /// difference in the bytes that get read twice has to be found, and so does
98    /// one in the bytes that are only read by the overlapping load.
99    #[test]
100    fn it_agrees_with_the_slice_comparison_at_every_length_and_position() {
101        for n in 0..40usize {
102            let a: Vec<u8> = (0..n).map(|i| (i % 251) as u8).collect();
103            assert!(bytes_eq(&a, &a.clone()), "{n} bytes against itself");
104            for at in 0..n {
105                let mut b = a.clone();
106                b[at] ^= 0x80;
107                assert!(!bytes_eq(&a, &b), "{n} bytes differing at {at}");
108                assert_eq!(bytes_eq(&a, &b), a == b);
109            }
110        }
111    }
112
113    #[test]
114    fn a_different_length_is_never_equal() {
115        for n in 0..40usize {
116            let a = vec![b'x'; n];
117            let b = vec![b'x'; n + 1];
118            assert!(!bytes_eq(&a, &b));
119            assert!(!bytes_eq(&b, &a));
120        }
121    }
122
123    #[test]
124    fn nothing_is_the_same_as_nothing() {
125        assert!(bytes_eq(b"", b""));
126        assert!(bytes_eq(&[], &[]));
127    }
128}