Skip to main content

rudb_vector/
fsst.rs

1//! FSST, the string encoding.
2//!
3//! Fast Static Symbol Table, from the 2020 paper by Boncz, Neumann and Leis. A table of at most 255
4//! symbols of one to eight bytes each, and compression is replacing the longest matching symbol at
5//! each position with its one byte code. A byte that no symbol covers is escaped, which costs two
6//! bytes, so the table has to be good or the output is larger than the input.
7//!
8//! ## Why this and not a general compressor
9//!
10//! `spec/06-compression.md` section 6.2 is blunt about it. FSST compresses text about 2x, which is
11//! worse than what zstd does to the same bytes, and the ratio is not why it is here. Two other
12//! properties are.
13//!
14//! The first is random access. Every string in a column is compressed independently against a
15//! shared table, so reading row 4,000,000 does not mean decompressing the four million before it. A
16//! block compressor gives up that property and gets it back by cutting the data into blocks, which
17//! means reading one string decompresses a block.
18//!
19//! The second is that a substring search can run against the compressed bytes. Compress the needle
20//! with the same symbol table and look for the compressed needle in the compressed haystack. That is
21//! what turns `URL LIKE '%google%'` from a decompress and scan into a scan, and section 6.7 says it
22//! is worth more on the ClickBench workload than any ratio improvement. It needs care, because the
23//! greedy match that compresses a needle standing alone can segment it differently from the way the
24//! same bytes were segmented inside a longer string, so a hit is a candidate and a miss is not a
25//! proof. The scan that uses it is M3 work and lives with the rest of encoded execution.
26//!
27//! ## Training
28//!
29//! The table is built from a sample rather than from the whole column, and the algorithm is the
30//! paper's: start with nothing, so every byte escapes, then repeat five times. Compress the sample
31//! with the table you have, count how often each symbol is used and how often each pair of adjacent
32//! symbols occurs, and build the next table from the best 255 of the symbols and the concatenations
33//! by gain, where gain is how many bytes of input the symbol accounts for. Five generations is what
34//! the paper found, and the shape of the thing is that the first generation learns single bytes, the
35//! second learns pairs, and the fifth is finding eight byte symbols like `https://`.
36//!
37//! ## Matching
38//!
39//! Three lookups in a fixed order, longest first. A hash table on the first three bytes for symbols
40//! of three bytes and up, a flat table indexed by the first two bytes, and a flat table indexed by
41//! the first one. The hash table probes eight slots and keeps the longest symbol that matches rather
42//! than the first, because several symbols share a three byte prefix and taking the first would make
43//! the ratio depend on insertion order.
44
45use std::cell::RefCell;
46use std::sync::OnceLock;
47
48use rudb_common::{Error, Result};
49
50/// The code that means the next byte is a literal. 255 rather than 0 so that the 255 real codes are
51/// a contiguous range starting at zero and a code is its own index into the symbol table.
52pub const ESCAPE: u8 = 255;
53
54/// How many real symbols a table can hold.
55pub const MAX_SYMBOLS: usize = 255;
56
57/// The longest a symbol can be. Eight, so that a symbol is a `u64` and a match is a mask and a
58/// compare rather than a loop over bytes.
59pub const MAX_SYMBOL_LEN: usize = 8;
60
61/// How many generations the trainer runs. The paper's number.
62const GENERATIONS: usize = 5;
63
64/// Slots in the prefix hash table. A power of two, and four times the largest number of symbols that
65/// can be in it, which keeps the eight slot probe from filling up on a full table.
66const HASH_SLOTS: usize = 1024;
67
68/// How far a lookup probes before giving up. A miss here costs ratio and not correctness.
69const PROBE: usize = 8;
70
71/// One symbol. The bytes are in the low end of `value` in the order they appear, so that a match
72/// against the next eight bytes of input is one mask and one compare.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
74struct Symbol {
75    value: u64,
76    len: u8,
77}
78
79impl Symbol {
80    fn new(bytes: &[u8]) -> Self {
81        let len = bytes.len().min(MAX_SYMBOL_LEN);
82        let mut value = 0u64;
83        for (index, byte) in bytes[..len].iter().enumerate() {
84            value |= u64::from(*byte) << (8 * index);
85        }
86        Self { value, len: len as u8 }
87    }
88
89    fn single(byte: u8) -> Self {
90        Self { value: u64::from(byte), len: 1 }
91    }
92
93    fn len(self) -> usize {
94        self.len as usize
95    }
96
97    fn mask(self) -> u64 {
98        mask_of(self.len())
99    }
100
101    fn bytes(self) -> Vec<u8> {
102        (0..self.len()).map(|index| (self.value >> (8 * index)) as u8).collect()
103    }
104
105    /// The two symbols end to end, cut off at eight bytes.
106    fn concat(self, other: Self) -> Self {
107        if self.len() >= MAX_SYMBOL_LEN {
108            return self;
109        }
110        let len = (self.len() + other.len()).min(MAX_SYMBOL_LEN);
111        let value = self.value | (other.value << (8 * self.len()));
112        Self { value: value & mask_of(len), len: len as u8 }
113    }
114}
115
116fn mask_of(len: usize) -> u64 {
117    if len >= 8 { u64::MAX } else { (1u64 << (8 * len)) - 1 }
118}
119
120/// A trained symbol table, and everything needed to compress and decompress against it.
121pub struct SymbolTable {
122    /// Code to symbol. At most [`MAX_SYMBOLS`] long.
123    symbols: Vec<Symbol>,
124    /// What compressing looks symbols up in, built the first time something is compressed.
125    ///
126    /// Decompressing only ever reads `symbols`, and a table read back from a file is almost always
127    /// read back to decompress. Building these eagerly filled a hundred and thirty kilobytes of
128    /// pair slots and sorted the symbols for every block of a text column a scan decoded, which on
129    /// `SELECT COUNT(*) FROM hits WHERE URL LIKE '%google%'` was six percent of the query.
130    lookup: OnceLock<Lookup>,
131}
132
133/// The tables the matcher reads, all of them built from the symbols and holding nothing else.
134struct Lookup {
135    /// First byte to code, or [`ESCAPE`] when no one byte symbol covers it. Only read for the last
136    /// byte of a string, where `short` would be looking at a padding byte as the second one.
137    single: Vec<u8>,
138    /// First two bytes to what matches there when no longer symbol does: the code in the low byte
139    /// and the length in the high one. That is the two byte symbol when there is one, and otherwise
140    /// what `single` says for the first byte with a length of one, so that the fallback is one load
141    /// rather than two lookups and a branch between them.
142    short: Vec<u16>,
143    /// Open addressed on the first three bytes, one slot for each three bytes that some symbol of
144    /// three bytes or more starts with, pointing at that symbol's group in `long`.
145    heads: Vec<Head>,
146    /// Every symbol of three bytes or more that a match can reach, grouped by their first three
147    /// bytes and longest first inside a group, so the first one in a group that matches is the one
148    /// to take.
149    long: Vec<(Symbol, u8)>,
150}
151
152/// One slot of [`Lookup::heads`].
153#[derive(Debug, Clone, Copy)]
154struct Head {
155    /// The first three bytes, or [`NO_HEAD`] for an empty slot.
156    key: u32,
157    /// Where the group starts in [`Lookup::long`] and how many symbols it has.
158    first: u16,
159    count: u16,
160}
161
162/// The key of an empty [`Head`], which no three bytes can be.
163const NO_HEAD: u32 = u32::MAX;
164
165impl std::fmt::Debug for SymbolTable {
166    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        // The lookup tables are 64k entries and printing them is never what anybody wanted.
168        formatter
169            .debug_struct("SymbolTable")
170            .field("symbols", &self.symbols.len())
171            .field("bytes", &self.serialized_len())
172            .finish()
173    }
174}
175
176/// Two tables are equal when they hold the same symbols in the same order.
177///
178/// The three lookup tables are built from the symbols when a table is built and hold nothing the
179/// symbols do not, so comparing them would be comparing the same information a second time over
180/// sixty five thousand entries. A vector in FSST form carries a table, and a vector is compared for
181/// equality all over the tests, so this is on a path that gets walked.
182impl PartialEq for SymbolTable {
183    fn eq(&self, other: &Self) -> bool {
184        self.symbols == other.symbols
185    }
186}
187
188impl Eq for SymbolTable {}
189
190impl SymbolTable {
191    /// How many bytes of memory this table is holding.
192    ///
193    /// Mostly the hash table, which is sixty five thousand slots however few symbols are in it. That
194    /// is the number a vector in FSST form reports, and it is why the form is a decision about a
195    /// page rather than about a chunk: one table over a hundred chunks is nothing per chunk and one
196    /// table per chunk is a megabyte.
197    #[must_use]
198    pub fn footprint(&self) -> usize {
199        size_of::<Self>()
200            + self.symbols.capacity() * size_of::<Symbol>()
201            + self.lookup.get().map_or(0, |lookup| {
202                lookup.single.capacity()
203                    + lookup.short.capacity() * size_of::<u16>()
204                    + lookup.heads.capacity() * size_of::<Head>()
205                    + lookup.long.capacity() * size_of::<(Symbol, u8)>()
206            })
207    }
208
209    /// A table with no symbols, which escapes everything and doubles its input. The starting point
210    /// of training, and what a column of nothing but unique bytes ends up with.
211    #[must_use]
212    pub fn empty() -> Self {
213        Self::build(Vec::new())
214    }
215
216    /// Trains a table on a sample.
217    ///
218    /// The caller picks the sample. Section 6.3 says a systematic sample across the chunk rather
219    /// than the first N rows, because column data is frequently clustered, and that decision belongs
220    /// to whoever knows what the chunk is rather than to this function.
221    #[must_use]
222    pub fn train(samples: &[&[u8]]) -> Self {
223        // The counts are a megabyte of pair slots, and a load trains a table for every block of
224        // every text column it writes. So each thread keeps one and clears the slots it used, rather
225        // than asking for a fresh megabyte of zeroes and faulting it in on every block.
226        thread_local! {
227            static COUNTS: RefCell<Option<Counts>> = const { RefCell::new(None) };
228        }
229        COUNTS.with(|held| match held.try_borrow_mut() {
230            Ok(mut held) => Self::train_with(samples, held.get_or_insert_with(Counts::new)),
231            Err(_) => Self::train_with(samples, &mut Counts::new()),
232        })
233    }
234
235    fn train_with(samples: &[&[u8]], counts: &mut Counts) -> Self {
236        let mut table = Self::empty();
237        for _ in 0..GENERATIONS {
238            counts.clear();
239            for sample in samples {
240                table.count(sample, counts);
241            }
242            let next = counts.best(&table);
243            if next.is_empty() {
244                break;
245            }
246            table = Self::build(next);
247        }
248        table
249    }
250
251    /// How many symbols are in the table.
252    #[must_use]
253    pub fn len(&self) -> usize {
254        self.symbols.len()
255    }
256
257    /// Whether the table has no symbols, in which case every byte of every string escapes.
258    #[must_use]
259    pub fn is_empty(&self) -> bool {
260        self.symbols.is_empty()
261    }
262
263    /// The bytes code `code` stands for, in the low end of eight, and how many of them there are.
264    ///
265    /// For a reader that walks codes rather than decompressing them, which needs to know what each
266    /// code spells once per table rather than once per string. `None` for a code past the table,
267    /// [`ESCAPE`] included.
268    #[must_use]
269    pub fn symbol(&self, code: u8) -> Option<([u8; MAX_SYMBOL_LEN], usize)> {
270        let symbol = self.symbols.get(code as usize)?;
271        Some((symbol.value.to_le_bytes(), symbol.len()))
272    }
273
274    /// How many bytes [`serialize`](Self::serialize) writes. At most 2049 for a full table, and
275    /// that is the number section 6.4 is weighing when it says a shared symbol table is cheaper
276    /// than a shared dictionary.
277    #[must_use]
278    pub fn serialized_len(&self) -> usize {
279        1 + self.symbols.iter().map(|symbol| 1 + symbol.len()).sum::<usize>()
280    }
281
282    /// Writes the table itself, which has to travel with the data it compressed.
283    pub fn serialize(&self, out: &mut Vec<u8>) {
284        out.push(self.symbols.len() as u8);
285        for symbol in &self.symbols {
286            out.push(symbol.len);
287            out.extend_from_slice(&symbol.bytes());
288        }
289    }
290
291    /// Reads back what [`serialize`](Self::serialize) wrote, and says how many bytes it consumed.
292    ///
293    /// # Errors
294    ///
295    /// If the bytes are truncated or describe a symbol of zero or more than eight bytes.
296    pub fn deserialize(bytes: &[u8]) -> Result<(Self, usize)> {
297        let count = *bytes.first().ok_or_else(|| truncated("a symbol table header"))? as usize;
298        let mut at = 1;
299        let mut symbols = Vec::with_capacity(count);
300        for _ in 0..count {
301            let len = *bytes.get(at).ok_or_else(|| truncated("a symbol length"))? as usize;
302            if len == 0 || len > MAX_SYMBOL_LEN {
303                return Err(Error::internal(format!("a symbol of {len} bytes is not a symbol")));
304            }
305            at += 1;
306            let end = at + len;
307            if end > bytes.len() {
308                return Err(truncated("a symbol"));
309            }
310            symbols.push(Symbol::new(&bytes[at..end]));
311            at = end;
312        }
313        Ok((Self::build(symbols), at))
314    }
315
316    /// Compresses one string, appending to `out`.
317    ///
318    /// Strings are compressed one at a time against a shared table rather than as one stream,
319    /// because that is what keeps random access, which is the first of the two reasons this encoding
320    /// was chosen at all.
321    pub fn compress(&self, input: &[u8], out: &mut Vec<u8>) {
322        let lookup = self.lookup();
323        let mut at = 0;
324        while at < input.len() {
325            let (code, len) = lookup.match_at(input, at);
326            if code == ESCAPE {
327                out.push(ESCAPE);
328                out.push(input[at]);
329            } else {
330                out.push(code);
331            }
332            at += len;
333        }
334    }
335
336    /// Decompresses one string, appending to `out`.
337    ///
338    /// A symbol goes out as all eight of the bytes its `u64` holds, and then the cursor steps back
339    /// over the ones that were not part of it. Eight is a length the compiler knows, so that is one
340    /// store. The length a symbol really has is only known at run time, so copying exactly that
341    /// many bytes is a call into `memcpy` for one to eight of them, and building a `Vec` to copy
342    /// them out of, which is what this used to do, is a heap allocation and a free on top.
343    ///
344    /// That mattered more than anything else in the engine. `SELECT COUNT(*) FROM hits WHERE URL
345    /// LIKE '%google%'` over ClickBench spends almost all of its time here, because the search
346    /// itself runs once per distinct URL and finding those means decompressing the column, and the
347    /// allocation, the free and the copy together were 41% of the query.
348    ///
349    /// # Errors
350    ///
351    /// If the input ends on an escape byte, or holds a code the table does not have.
352    pub fn decompress(&self, input: &[u8], out: &mut Vec<u8>) -> Result<()> {
353        // What the table is trained to reach, plus room for the tail of the last symbol, so the
354        // loop below mostly finds the space already there. Nothing here depends on the guess being
355        // right: too small and the growth happens where it always did.
356        out.reserve(input.len().saturating_mul(2).saturating_add(MAX_SYMBOL_LEN));
357        let mut at = 0;
358        while at < input.len() {
359            let code = input[at];
360            at += 1;
361            if code == ESCAPE {
362                let literal = *input.get(at).ok_or_else(|| truncated("an escaped byte"))?;
363                out.push(literal);
364                at += 1;
365            } else {
366                let symbol = *self
367                    .symbols
368                    .get(code as usize)
369                    .ok_or_else(|| Error::internal(format!("code {code} is not in the table")))?;
370                out.extend_from_slice(&symbol.value.to_le_bytes());
371                out.truncate(out.len() - (MAX_SYMBOL_LEN - symbol.len()));
372            }
373        }
374        Ok(())
375    }
376
377    /// Decompresses one string into `out` from `at`, handing back where it ended.
378    ///
379    /// [`Self::decompress`] for a caller that has already made the buffer as long as the output
380    /// is going to be, so a symbol is one eight byte store and a step of the cursor with no length
381    /// to keep and no capacity to check. The store is eight bytes whatever the symbol's length,
382    /// which is why `out` needs [`MAX_SYMBOL_LEN`] bytes of room past the end of the string.
383    ///
384    /// # Errors
385    ///
386    /// As [`Self::decompress`], and if `out` runs out of room, which a string that really
387    /// decompresses to the length the caller sized for never does.
388    ///
389    /// Inlined because a caller replaying a chunk asks for about nine short runs per value, and as
390    /// a call the pushes, pops and return around each one were a third of the time spent in here.
391    #[inline]
392    pub fn decompress_at(&self, input: &[u8], out: &mut [u8], mut at: usize) -> Result<usize> {
393        let symbols = self.symbols.as_slice();
394        let mut codes = input.iter();
395        while let Some(&code) = codes.next() {
396            if code == ESCAPE {
397                let Some(&literal) = codes.next() else {
398                    return Err(truncated("an escaped byte"));
399                };
400                let Some(slot) = out.get_mut(at) else {
401                    return Err(out_of_room());
402                };
403                *slot = literal;
404                at += 1;
405            } else {
406                let Some(symbol) = symbols.get(code as usize) else {
407                    return Err(not_in_table(code));
408                };
409                let Some(slot) = out.get_mut(at..at + MAX_SYMBOL_LEN) else {
410                    return Err(out_of_room());
411                };
412                slot.copy_from_slice(&symbol.value.to_le_bytes());
413                at += symbol.len();
414            }
415        }
416        Ok(at)
417    }
418
419    /// Runs the matcher over a sample without producing output, recording what it used. This is the
420    /// counting half of a training generation.
421    fn count(&self, input: &[u8], counts: &mut Counts) {
422        let lookup = self.lookup();
423        let mut at = 0;
424        let mut previous: Option<u16> = None;
425        while at < input.len() {
426            let (code, len) = lookup.match_at(input, at);
427            let id = if code == ESCAPE { 256 + u16::from(input[at]) } else { u16::from(code) };
428            counts.one(id);
429            if let Some(previous) = previous {
430                counts.two(previous, id);
431            }
432            previous = Some(id);
433            at += len;
434        }
435    }
436
437    fn build(symbols: Vec<Symbol>) -> Self {
438        Self { symbols, lookup: OnceLock::new() }
439    }
440
441    fn lookup(&self) -> &Lookup {
442        self.lookup.get_or_init(|| Lookup::of(&self.symbols))
443    }
444}
445
446impl Lookup {
447    fn of(symbols: &[Symbol]) -> Self {
448        let mut single = vec![ESCAPE; 256];
449        let mut pair = vec![u16::MAX; 65536];
450        // Where each long symbol would sit in a table probed PROBE slots from the hash of its first
451        // three bytes. That table is not kept, since a match only needs the groups below, but it
452        // is what decides which long symbols a match can reach at all: one that finds no free slot
453        // in its window is never matched, and that has to stay true for a string to compress to
454        // the same bytes it always has.
455        let mut placed: Vec<Option<(Symbol, u8)>> = vec![None; HASH_SLOTS];
456        let mut long = Vec::new();
457        // Longest first, so that a short symbol never displaces a long one out of the probe window
458        // and the flat tables get the lowest code for a duplicate.
459        let mut order: Vec<(Symbol, u8)> =
460            symbols.iter().enumerate().map(|(code, symbol)| (*symbol, code as u8)).collect();
461        order.sort_by_key(|(symbol, code)| (std::cmp::Reverse(symbol.len()), *code));
462        for (symbol, code) in order {
463            match symbol.len() {
464                1 => {
465                    let index = (symbol.value & 0xff) as usize;
466                    if single[index] == ESCAPE {
467                        single[index] = code;
468                    }
469                }
470                2 => {
471                    let index = (symbol.value & 0xffff) as usize;
472                    if pair[index] == u16::MAX {
473                        pair[index] = u16::from(code);
474                    }
475                }
476                _ => {
477                    let mut slot = hash_of(symbol.value);
478                    for _ in 0..PROBE {
479                        if placed[slot].is_none() {
480                            placed[slot] = Some((symbol, code));
481                            long.push((symbol, code));
482                            break;
483                        }
484                        slot = (slot + 1) & (HASH_SLOTS - 1);
485                    }
486                }
487            }
488        }
489        // `long` is in the order the symbols went in, longest first and then by code, and a stable
490        // sort on the first three bytes keeps that order inside each group. The probe used to keep
491        // the longest match and the first of equals, which is the first match in that order.
492        long.sort_by_key(|(symbol, _)| symbol.value & 0xff_ffff);
493        let mut heads = vec![Head { key: NO_HEAD, first: 0, count: 0 }; HASH_SLOTS];
494        let mut start = 0;
495        while start < long.len() {
496            let key = long[start].0.value & 0xff_ffff;
497            let mut end = start + 1;
498            while end < long.len() && long[end].0.value & 0xff_ffff == key {
499                end += 1;
500            }
501            // There are at most 255 symbols, so there are fewer groups than slots and the probe
502            // finds a free one.
503            let mut slot = hash_of(key);
504            while heads[slot].key != NO_HEAD {
505                slot = (slot + 1) & (HASH_SLOTS - 1);
506            }
507            heads[slot] =
508                Head { key: key as u32, first: start as u16, count: (end - start) as u16 };
509            start = end;
510        }
511        let short = (0..65536usize)
512            .map(|index| {
513                if pair[index] == u16::MAX {
514                    u16::from(single[index & 0xff]) | 1 << 8
515                } else {
516                    pair[index] | 2 << 8
517                }
518            })
519            .collect();
520        Self { single, short, heads, long }
521    }
522
523    /// The code and how many input bytes it covers. [`ESCAPE`] and 1 when nothing matches.
524    ///
525    /// Always inlined, because as a call the saving and restoring of registers around it cost as
526    /// much as a lookup that finds its symbol in the first slot.
527    #[inline(always)]
528    fn match_at(&self, input: &[u8], at: usize) -> (u8, usize) {
529        let remaining = input.len() - at;
530        let word = load(input, at);
531        // Written as a nested `if` rather than as a chained `if let` because the minimum supported
532        // Rust version is 1.85 and let chains landed in 1.88.
533        if remaining >= 3 {
534            if let Some(found) = self.long_at(word, remaining) {
535                return found;
536            }
537        }
538        if remaining >= 2 {
539            let entry = self.short[(word & 0xffff) as usize];
540            return ((entry & 0xff) as u8, usize::from(entry >> 8));
541        }
542        (self.single[(word & 0xff) as usize], 1)
543    }
544
545    /// The longest symbol of three bytes or more matching here, if any, as its code and length.
546    ///
547    /// Longest rather than first, because several symbols share a three byte prefix and taking
548    /// whichever came first would make the compression ratio depend on the order the table was
549    /// built in. The group is in longest first order, so the first match is the longest.
550    #[inline]
551    fn long_at(&self, word: u64, remaining: usize) -> Option<(u8, usize)> {
552        let key = (word & 0xff_ffff) as u32;
553        let mut slot = hash_of(word);
554        loop {
555            let head = self.heads[slot];
556            if head.key == key {
557                let group = &self.long[usize::from(head.first)..][..usize::from(head.count)];
558                return group
559                    .iter()
560                    .find(|(symbol, _)| {
561                        symbol.len() <= remaining && word & symbol.mask() == symbol.value
562                    })
563                    .map(|(symbol, code)| (*code, symbol.len()));
564            }
565            if head.key == NO_HEAD {
566                return None;
567            }
568            slot = (slot + 1) & (HASH_SLOTS - 1);
569        }
570    }
571}
572
573/// The next eight bytes as a little endian word, zero padded at the end of the input.
574///
575/// The padding is why every match checks the remaining length as well as the mask. Without that
576/// check a two byte symbol ending in a zero byte would match the last byte of a string.
577///
578/// Near the end of a string that is at least eight bytes long, the word is the last eight bytes
579/// shifted down past the ones before `at`, which is one load where reading the tail a byte at a
580/// time was a loop at seven places in every string.
581#[inline]
582fn load(input: &[u8], at: usize) -> u64 {
583    if at + 8 <= input.len() {
584        let bytes: [u8; 8] = input[at..at + 8].try_into().expect("eight bytes were checked");
585        u64::from_le_bytes(bytes)
586    } else if input.len() >= 8 {
587        let bytes: [u8; 8] = input[input.len() - 8..].try_into().expect("eight bytes were checked");
588        // `at` is inside the last eight bytes and not at their start, so this shifts by eight to
589        // fifty six bits.
590        u64::from_le_bytes(bytes) >> (8 * (at + 8 - input.len()))
591    } else {
592        let mut word = 0u64;
593        for (index, byte) in input[at..].iter().enumerate() {
594            word |= u64::from(*byte) << (8 * index);
595        }
596        word
597    }
598}
599
600/// Hashes the first three bytes. The multiply and shift is the standard Fibonacci hash, which
601/// spreads a three byte key across the whole slot range where a mask of the low bits would put every
602/// symbol starting with the same letter in the same neighbourhood.
603fn hash_of(word: u64) -> usize {
604    let key = word & 0xff_ffff;
605    ((key.wrapping_mul(0x9e37_79b9_7f4a_7c15)) >> (64 - HASH_SLOTS.trailing_zeros())) as usize
606}
607
608/// What one training generation counts. Symbol ids below 256 are codes in the current table and ids
609/// from 256 up are escaped literal bytes, which is how a generation learns single bytes it does not
610/// have yet.
611struct Counts {
612    single: Vec<u32>,
613    /// Indexed by `first * IDS + second`. Flat rather than hashed because every id is under 512, so
614    /// every pair fits in a quarter million slots, and counting pairs is most of what training does.
615    pairs: Vec<u32>,
616    /// The pair slots that are not zero, so that reading and clearing them costs the pairs seen
617    /// rather than the whole array.
618    seen: Vec<u32>,
619    /// The candidates of the generation being ranked, kept so a thread sizes them once.
620    gains: Gains,
621}
622
623/// Every candidate symbol once with its summed gain, which [`Counts::best`] ranks.
624#[derive(Default)]
625struct Gains {
626    gains: Vec<(Symbol, u64)>,
627    /// Where each symbol sits in `gains`, open addressed on the symbol, `u32::MAX` where empty.
628    places: Vec<u32>,
629    /// The slots of `places` in use, so that emptying it costs the symbols rather than the table.
630    taken: Vec<u32>,
631}
632
633impl Gains {
634    /// Empties the candidates, with room for `most` of them.
635    fn clear(&mut self, most: usize) {
636        for place in self.taken.drain(..) {
637            self.places[place as usize] = u32::MAX;
638        }
639        let wanted = (most * 2).next_power_of_two();
640        if self.places.len() < wanted {
641            self.places = vec![u32::MAX; wanted];
642        }
643        self.gains.clear();
644    }
645
646    /// Adds `count` uses of `symbol` to its gain, making it a candidate if it is not one yet.
647    fn add(&mut self, symbol: Symbol, count: u64) {
648        let gain = count * symbol.len() as u64;
649        let mask = self.places.len() - 1;
650        let mut place = gain_hash(symbol) & mask;
651        loop {
652            let at = self.places[place];
653            if at == u32::MAX {
654                self.places[place] = self.gains.len() as u32;
655                self.taken.push(place as u32);
656                self.gains.push((symbol, gain));
657                return;
658            }
659            if self.gains[at as usize].0 == symbol {
660                self.gains[at as usize].1 += gain;
661                return;
662            }
663            place = (place + 1) & mask;
664        }
665    }
666}
667
668/// How many symbol ids there are: 256 codes and 256 escaped bytes.
669const IDS: usize = 512;
670
671impl Counts {
672    fn new() -> Self {
673        Self {
674            single: vec![0; IDS],
675            pairs: vec![0; IDS * IDS],
676            seen: Vec::new(),
677            gains: Gains::default(),
678        }
679    }
680
681    fn clear(&mut self) {
682        self.single.fill(0);
683        for slot in self.seen.drain(..) {
684            self.pairs[slot as usize] = 0;
685        }
686    }
687
688    fn one(&mut self, id: u16) {
689        self.single[id as usize] += 1;
690    }
691
692    fn two(&mut self, first: u16, second: u16) {
693        let slot = first as usize * IDS + second as usize;
694        if self.pairs[slot] == 0 {
695            self.seen.push(slot as u32);
696        }
697        self.pairs[slot] += 1;
698    }
699
700    /// The 255 best symbols for the next generation.
701    ///
702    /// Gain is how many bytes of input a symbol accounts for, which is its length times how often it
703    /// was used. A concatenation is scored on the length it would have, so a pair of four byte
704    /// symbols scores as eight and a pair of six byte ones also scores as eight, because that is
705    /// what it would be cut down to.
706    fn best(&mut self, table: &SymbolTable) -> Vec<Symbol> {
707        // Different ids can spell the same symbol, a code and the pair it was learned from for one,
708        // and two pairs whose concatenation runs past eight bytes for another, so the gains are
709        // summed per symbol before anything is ranked. This was a sort of every candidate by symbol
710        // followed by a dedup, and on a ClickBench load that sort was a quarter of training.
711        self.gains.clear(IDS + self.seen.len());
712        for (id, count) in self.single.iter().enumerate() {
713            if *count == 0 {
714                continue;
715            }
716            let symbol = symbol_of(table, id as u16);
717            self.gains.add(symbol, u64::from(*count));
718        }
719        for slot in &self.seen {
720            let slot = *slot as usize;
721            let (first, second) = ((slot / IDS) as u16, (slot % IDS) as u16);
722            let symbol = symbol_of(table, first).concat(symbol_of(table, second));
723            self.gains.add(symbol, u64::from(self.pairs[slot]));
724        }
725        let gains = &mut self.gains.gains;
726        // Gain first, then the symbol itself, so that two symbols with the same gain come out in the
727        // same order on every host and the table is a function of the sample and nothing else. The
728        // symbols are distinct by now, so the order is total and an unstable sort gives one answer.
729        let order = |left: &(Symbol, u64), right: &(Symbol, u64)| {
730            right.1.cmp(&left.1).then(left.0.cmp(&right.0))
731        };
732        if gains.len() > MAX_SYMBOLS {
733            gains.select_nth_unstable_by(MAX_SYMBOLS - 1, order);
734            gains.truncate(MAX_SYMBOLS);
735        }
736        gains.sort_unstable_by(order);
737        gains.iter().map(|(symbol, _)| *symbol).collect()
738    }
739}
740
741/// Where a symbol's search for its place in [`Gains::places`] starts.
742fn gain_hash(symbol: Symbol) -> usize {
743    ((symbol.value ^ u64::from(symbol.len)).wrapping_mul(0x9e37_79b9_7f4a_7c15) >> 32) as usize
744}
745
746fn symbol_of(table: &SymbolTable, id: u16) -> Symbol {
747    if (id as usize) < table.symbols.len() {
748        table.symbols[id as usize]
749    } else {
750        Symbol::single((id.saturating_sub(256)) as u8)
751    }
752}
753
754#[cold]
755fn not_in_table(code: u8) -> Error {
756    Error::internal(format!("code {code} is not in the table"))
757}
758
759#[cold]
760fn out_of_room() -> Error {
761    Error::internal("a string decompresses to more than its length says")
762}
763
764#[cold]
765fn truncated(what: &str) -> Error {
766    Error::internal(format!("the input ended in the middle of {what}"))
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772
773    #[test]
774    fn a_table_read_back_to_decompress_builds_no_lookup_tables() {
775        let urls = urls();
776        let samples: Vec<&[u8]> = urls.iter().map(Vec::as_slice).collect();
777        let trained = SymbolTable::train(&samples);
778        let mut compressed = Vec::new();
779        trained.compress(&urls[7], &mut compressed);
780        let mut stored = Vec::new();
781        trained.serialize(&mut stored);
782        let (read, _) = SymbolTable::deserialize(&stored).expect("a table it wrote");
783        let mut out = Vec::new();
784        read.decompress(&compressed, &mut out).expect("a string it compressed");
785        assert_eq!(out, urls[7]);
786        assert!(read.lookup.get().is_none(), "decompressing reads the symbols alone");
787        assert!(read.footprint() < trained.footprint());
788    }
789
790    /// A few hundred URLs in the shape ClickBench `hits` has them, which is the workload this
791    /// encoding was chosen for. Repetitive in the way real URLs are: a handful of hosts, a handful
792    /// of path shapes, and query strings that differ in a number.
793    fn urls() -> Vec<Vec<u8>> {
794        let hosts = ["www.example.com", "shop.example.com", "news.other.example.org"];
795        let paths = ["/index.html", "/catalog/item", "/search", "/user/profile/settings"];
796        let mut out = Vec::new();
797        for index in 0..600 {
798            let host = hosts[index % hosts.len()];
799            let path = paths[(index / 3) % paths.len()];
800            out.push(
801                format!("http://{host}{path}?session={}&ref=google&page={}", index * 7, index % 20)
802                    .into_bytes(),
803            );
804        }
805        out
806    }
807
808    fn borrow(strings: &[Vec<u8>]) -> Vec<&[u8]> {
809        strings.iter().map(Vec::as_slice).collect()
810    }
811
812    fn round_trip(table: &SymbolTable, strings: &[Vec<u8>]) -> (usize, usize) {
813        let mut raw = 0;
814        let mut compressed = 0;
815        for string in strings {
816            let mut bytes = Vec::new();
817            table.compress(string, &mut bytes);
818            let mut back = Vec::new();
819            table.decompress(&bytes, &mut back).unwrap();
820            assert_eq!(back, *string, "{}", String::from_utf8_lossy(string));
821            raw += string.len();
822            compressed += bytes.len();
823        }
824        (raw, compressed)
825    }
826
827    #[test]
828    fn urls_compress_by_more_than_half_and_come_back_unchanged() {
829        // The number the paper reports on text is around 2x, and URLs are more repetitive than
830        // text. Anything under 2x here means the trainer is not finding the long symbols.
831        let strings = urls();
832        let table = SymbolTable::train(&borrow(&strings));
833        let (raw, compressed) = round_trip(&table, &strings);
834        let ratio = raw as f64 / compressed as f64;
835        assert!(ratio > 2.5, "{ratio:.2}x, {raw} to {compressed}");
836        assert!(table.len() > 100, "{} symbols", table.len());
837    }
838
839    #[test]
840    fn the_trainer_finds_the_long_repeated_pieces() {
841        let strings = urls();
842        let table = SymbolTable::train(&borrow(&strings));
843        let found: Vec<String> = (0..table.len())
844            .map(|code| String::from_utf8_lossy(&table.symbols[code].bytes()).into_owned())
845            .collect();
846        // Not a specific symbol, since which eight bytes win is a property of the sample, but the
847        // table has to be mostly long symbols or it has not learned anything.
848        let long = found.iter().filter(|symbol| symbol.len() >= 6).count();
849        assert!(long > 60, "only {long} symbols of six bytes or more: {found:?}");
850    }
851
852    #[test]
853    fn english_text_round_trips_and_shrinks() {
854        let text: Vec<Vec<u8>> = "the quick brown fox jumps over the lazy dog while the other dog \
855             watches the fox and the dog and the fox go over the hill together"
856            .split(' ')
857            .map(|word| word.as_bytes().to_vec())
858            .collect();
859        let table = SymbolTable::train(&borrow(&text));
860        let (raw, compressed) = round_trip(&table, &text);
861        assert!(compressed < raw, "{raw} to {compressed}");
862    }
863
864    #[test]
865    fn incompressible_bytes_round_trip_and_cost_what_escaping_costs() {
866        // The worst case, and it has to be a correct worst case. Every byte escapes at two bytes
867        // each unless the trainer finds single byte symbols, which it will for the 255 most common
868        // of the 256 values.
869        let mut state = 0x1234_5678_9abc_def0u64;
870        let strings: Vec<Vec<u8>> = (0..100)
871            .map(|_| {
872                (0..64)
873                    .map(|_| {
874                        state ^= state << 13;
875                        state ^= state >> 7;
876                        state ^= state << 17;
877                        state as u8
878                    })
879                    .collect()
880            })
881            .collect();
882        let table = SymbolTable::train(&borrow(&strings));
883        let (raw, compressed) = round_trip(&table, &strings);
884        assert!(compressed < raw * 2, "{raw} to {compressed}");
885    }
886
887    #[test]
888    fn an_empty_table_escapes_everything_and_still_round_trips() {
889        let table = SymbolTable::empty();
890        let strings = vec![b"hello".to_vec(), Vec::new(), b"x".to_vec()];
891        let (raw, compressed) = round_trip(&table, &strings);
892        assert_eq!(compressed, raw * 2);
893    }
894
895    #[test]
896    fn an_empty_string_compresses_to_nothing() {
897        let table = SymbolTable::train(&[b"abcabcabc"]);
898        let mut out = Vec::new();
899        table.compress(b"", &mut out);
900        assert!(out.is_empty());
901        let mut back = Vec::new();
902        table.decompress(&out, &mut back).unwrap();
903        assert!(back.is_empty());
904    }
905
906    #[test]
907    fn a_string_shorter_than_the_symbols_does_not_read_past_its_end() {
908        // The load pads with zeros, so without the length check a two byte symbol whose second byte
909        // is zero would match the last byte of a string and swallow a byte that is not there.
910        let table = SymbolTable::train(&[b"ab\0ab\0ab\0ab\0", b"abcdefgh"]);
911        for string in [b"a".to_vec(), b"ab".to_vec(), b"abc".to_vec()] {
912            let mut bytes = Vec::new();
913            table.compress(&string, &mut bytes);
914            let mut back = Vec::new();
915            table.decompress(&bytes, &mut back).unwrap();
916            assert_eq!(back, string);
917        }
918    }
919
920    #[test]
921    fn a_table_survives_being_written_and_read_back() {
922        let strings = urls();
923        let table = SymbolTable::train(&borrow(&strings));
924        let mut bytes = Vec::new();
925        table.serialize(&mut bytes);
926        assert_eq!(bytes.len(), table.serialized_len());
927        let (read, consumed) = SymbolTable::deserialize(&bytes).unwrap();
928        assert_eq!(consumed, bytes.len());
929        assert_eq!(read.symbols, table.symbols);
930
931        // And the read back table compresses to the same bytes, which is the property that matters,
932        // since the lookup structures are rebuilt rather than stored.
933        let mut first = Vec::new();
934        let mut second = Vec::new();
935        table.compress(&strings[7], &mut first);
936        read.compress(&strings[7], &mut second);
937        assert_eq!(first, second);
938    }
939
940    #[test]
941    fn a_full_table_is_two_kilobytes_at_the_very_most() {
942        let strings = urls();
943        let table = SymbolTable::train(&borrow(&strings));
944        assert!(table.serialized_len() <= 1 + MAX_SYMBOLS * (1 + MAX_SYMBOL_LEN));
945        assert!(table.serialized_len() <= 2049);
946    }
947
948    #[test]
949    fn a_truncated_symbol_table_is_an_error() {
950        let strings = urls();
951        let table = SymbolTable::train(&borrow(&strings));
952        let mut bytes = Vec::new();
953        table.serialize(&mut bytes);
954        for len in 1..bytes.len().min(40) {
955            let error = SymbolTable::deserialize(&bytes[..len]).unwrap_err();
956            assert!(error.message().contains("ended in the middle"), "{error}");
957        }
958    }
959
960    #[test]
961    fn a_symbol_of_zero_bytes_is_an_error() {
962        let error = SymbolTable::deserialize(&[1, 0]).unwrap_err();
963        assert!(error.message().contains("is not a symbol"), "{error}");
964    }
965
966    #[test]
967    fn a_short_symbol_does_not_drag_the_rest_of_its_word_out_with_it() {
968        // Decompression writes all eight bytes of a symbol and steps back over the ones that were
969        // not part of it, so a table of short symbols is where that would show. `ab` and `cd` are
970        // two bytes each and sit in a `u64` with six zero bytes above them, and if the step back
971        // were wrong those zeros would be in the answer. Appending twice checks it again at an
972        // offset, since the second write lands where the first one left the cursor.
973        let table = SymbolTable::train(&[b"abcdabcdabcdabcd"]);
974        let mut compressed = Vec::new();
975        table.compress(b"abcdabcd", &mut compressed);
976        let mut out = Vec::new();
977        table.decompress(&compressed, &mut out).expect("decompresses");
978        table.decompress(&compressed, &mut out).expect("decompresses");
979        assert_eq!(out, b"abcdabcdabcdabcd");
980    }
981
982    #[test]
983    fn a_dangling_escape_is_an_error_and_not_a_panic() {
984        let table = SymbolTable::train(&[b"abcabcabc"]);
985        let error = table.decompress(&[ESCAPE], &mut Vec::new()).unwrap_err();
986        assert!(error.message().contains("escaped byte"), "{error}");
987    }
988
989    #[test]
990    fn a_code_the_table_does_not_have_is_an_error() {
991        let table = SymbolTable::train(&[b"abcabcabc"]);
992        let code = table.len() as u8;
993        let error = table.decompress(&[code], &mut Vec::new()).unwrap_err();
994        assert!(error.message().contains("not in the table"), "{error}");
995    }
996
997    #[test]
998    fn training_twice_on_the_same_sample_gives_the_same_table() {
999        // A table that differs run to run would make every size in the M1 report unreproducible.
1000        let strings = urls();
1001        let first = SymbolTable::train(&borrow(&strings));
1002        let second = SymbolTable::train(&borrow(&strings));
1003        assert_eq!(first.symbols, second.symbols);
1004    }
1005
1006    #[test]
1007    fn the_longest_match_wins_rather_than_the_first_one_found() {
1008        let table = SymbolTable::build(vec![
1009            Symbol::new(b"abc"),
1010            Symbol::new(b"abcdef"),
1011            Symbol::new(b"abcd"),
1012        ]);
1013        let mut out = Vec::new();
1014        table.compress(b"abcdef", &mut out);
1015        assert_eq!(out, vec![1]);
1016    }
1017
1018    #[test]
1019    fn a_symbol_longer_than_what_is_left_is_not_used() {
1020        let table = SymbolTable::build(vec![Symbol::new(b"abcdef"), Symbol::new(b"ab")]);
1021        let mut out = Vec::new();
1022        table.compress(b"abcd", &mut out);
1023        // "ab" then two escapes, rather than a six byte symbol over four bytes of input.
1024        assert_eq!(out, vec![1, ESCAPE, b'c', ESCAPE, b'd']);
1025    }
1026
1027    /// The matcher the way it was before the long symbols were grouped by their first three bytes:
1028    /// a probe of up to eight slots from the hash, keeping the longest match, then the pair table,
1029    /// then the single one.
1030    fn match_by_probe(symbols: &[Symbol], input: &[u8], at: usize) -> (u8, usize) {
1031        let mut single = vec![ESCAPE; 256];
1032        let mut pair = vec![u16::MAX; 65536];
1033        let mut hash: Vec<Option<(Symbol, u8)>> = vec![None; HASH_SLOTS];
1034        let mut order: Vec<(Symbol, u8)> =
1035            symbols.iter().enumerate().map(|(code, symbol)| (*symbol, code as u8)).collect();
1036        order.sort_by_key(|(symbol, code)| (std::cmp::Reverse(symbol.len()), *code));
1037        for (symbol, code) in order {
1038            match symbol.len() {
1039                1 if single[(symbol.value & 0xff) as usize] == ESCAPE => {
1040                    single[(symbol.value & 0xff) as usize] = code;
1041                }
1042                2 if pair[(symbol.value & 0xffff) as usize] == u16::MAX => {
1043                    pair[(symbol.value & 0xffff) as usize] = u16::from(code);
1044                }
1045                1 | 2 => {}
1046                _ => {
1047                    let mut slot = hash_of(symbol.value);
1048                    for _ in 0..PROBE {
1049                        if hash[slot].is_none() {
1050                            hash[slot] = Some((symbol, code));
1051                            break;
1052                        }
1053                        slot = (slot + 1) & (HASH_SLOTS - 1);
1054                    }
1055                }
1056            }
1057        }
1058        let remaining = input.len() - at;
1059        let word = load(input, at);
1060        if remaining >= 3 {
1061            let mut slot = hash_of(word);
1062            let mut best: Option<(Symbol, u8)> = None;
1063            for _ in 0..PROBE {
1064                let Some((symbol, code)) = hash[slot] else { break };
1065                if symbol.len() <= remaining
1066                    && word & symbol.mask() == symbol.value
1067                    && best.is_none_or(|(found, _)| symbol.len() > found.len())
1068                {
1069                    best = Some((symbol, code));
1070                }
1071                slot = (slot + 1) & (HASH_SLOTS - 1);
1072            }
1073            if let Some((symbol, code)) = best {
1074                return (code, symbol.len());
1075            }
1076        }
1077        if remaining >= 2 {
1078            let code = pair[(word & 0xffff) as usize];
1079            if code != u16::MAX {
1080                return (code as u8, 2);
1081            }
1082        }
1083        (single[(word & 0xff) as usize], 1)
1084    }
1085
1086    #[test]
1087    fn grouping_the_long_symbols_matches_what_the_probe_matched() {
1088        // A small alphabet and a full table, so that many long symbols share their first three
1089        // bytes, some clusters overflow the probe window, and a symbol can end in a zero byte.
1090        let alphabet = b"ab\0c";
1091        let mut seed = 0x2545_f491_4f6c_dd1du64;
1092        let mut next = move |below: usize| {
1093            seed ^= seed << 13;
1094            seed ^= seed >> 7;
1095            seed ^= seed << 17;
1096            (seed % below as u64) as usize
1097        };
1098        for round in 0..40 {
1099            let mut symbols = Vec::new();
1100            while symbols.len() < MAX_SYMBOLS {
1101                let len = 1 + next(MAX_SYMBOL_LEN);
1102                let bytes: Vec<u8> = (0..len).map(|_| alphabet[next(alphabet.len())]).collect();
1103                symbols.push(Symbol::new(&bytes));
1104            }
1105            let table = SymbolTable::build(symbols.clone());
1106            for _ in 0..50 {
1107                let input: Vec<u8> =
1108                    (0..next(40)).map(|_| alphabet[next(alphabet.len())]).collect();
1109                for at in 0..input.len() {
1110                    assert_eq!(
1111                        table.lookup().match_at(&input, at),
1112                        match_by_probe(&symbols, &input, at),
1113                        "round {round}, {input:?} at {at}"
1114                    );
1115                }
1116            }
1117        }
1118    }
1119
1120    #[test]
1121    fn concatenation_stops_at_eight_bytes() {
1122        let long = Symbol::new(b"abcdef");
1123        assert_eq!(long.concat(Symbol::new(b"ghijkl")).bytes(), b"abcdefgh");
1124        assert_eq!(Symbol::new(b"ab").concat(Symbol::new(b"cd")).bytes(), b"abcd");
1125    }
1126
1127    /// The trainer the way it was written first, with the pairs and the gains in hash maps and one
1128    /// stable sort over everything, kept here to check the flat counts pick the same symbols.
1129    fn train_with_maps(samples: &[&[u8]]) -> SymbolTable {
1130        use std::collections::HashMap;
1131        let mut table = SymbolTable::empty();
1132        for _ in 0..GENERATIONS {
1133            let mut single = [0u32; IDS];
1134            let mut pairs: HashMap<(u16, u16), u32> = HashMap::new();
1135            for sample in samples {
1136                let mut at = 0;
1137                let mut previous: Option<u16> = None;
1138                while at < sample.len() {
1139                    let (code, len) = table.lookup().match_at(sample, at);
1140                    let id =
1141                        if code == ESCAPE { 256 + u16::from(sample[at]) } else { u16::from(code) };
1142                    single[id as usize] += 1;
1143                    if let Some(previous) = previous {
1144                        *pairs.entry((previous, id)).or_insert(0) += 1;
1145                    }
1146                    previous = Some(id);
1147                    at += len;
1148                }
1149            }
1150            let mut gains: HashMap<Symbol, u64> = HashMap::new();
1151            for (id, count) in single.iter().enumerate().filter(|(_, count)| **count > 0) {
1152                let symbol = symbol_of(&table, id as u16);
1153                *gains.entry(symbol).or_insert(0) += u64::from(*count) * symbol.len() as u64;
1154            }
1155            for ((first, second), count) in &pairs {
1156                let symbol = symbol_of(&table, *first).concat(symbol_of(&table, *second));
1157                *gains.entry(symbol).or_insert(0) += u64::from(*count) * symbol.len() as u64;
1158            }
1159            let mut ranked: Vec<(Symbol, u64)> = gains.into_iter().collect();
1160            ranked.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
1161            ranked.truncate(MAX_SYMBOLS);
1162            if ranked.is_empty() {
1163                break;
1164            }
1165            table = SymbolTable::build(ranked.into_iter().map(|(symbol, _)| symbol).collect());
1166        }
1167        table
1168    }
1169
1170    #[test]
1171    fn flat_counts_train_the_same_table_as_hash_maps() {
1172        let mut state = 0x9e37_79b9_7f4a_7c15u64;
1173        let mut next = move || {
1174            state ^= state << 13;
1175            state ^= state >> 7;
1176            state ^= state << 17;
1177            state
1178        };
1179        let mut shapes: Vec<Vec<Vec<u8>>> = vec![urls(), Vec::new(), vec![Vec::new(); 3]];
1180        // Few letters, so that many pairs tie on gain and the tie break decides the table.
1181        shapes.push(
1182            (0..400).map(|_| (0..12).map(|_| b"abc"[(next() % 3) as usize]).collect()).collect(),
1183        );
1184        // Every byte value, so that escapes of all 256 bytes are counted.
1185        shapes.push((0..300).map(|_| (0..40).map(|_| next() as u8).collect()).collect());
1186        // Long repeats, so that concatenations reach eight bytes and get cut.
1187        shapes.push(
1188            (0..200)
1189                .map(|index| format!("prefix-{}-suffix-{}", index % 7, index % 5).into_bytes())
1190                .collect(),
1191        );
1192        // One after another on one thread, so every table after the first is trained on the counts
1193        // the one before it left behind, which is what a load does block after block.
1194        for strings in &shapes {
1195            let samples = borrow(strings);
1196            assert_eq!(SymbolTable::train(&samples).symbols, train_with_maps(&samples).symbols);
1197        }
1198    }
1199}