structured_zstd/dictionary/
legacy.rs1use alloc::vec;
13use alloc::vec::Vec;
14
15use super::suffix_array::suffix_array;
16use crate::encoding::fastpath::{
17 FastpathKernel, dispatch_common_prefix_len_ptr_with_kernel, select_kernel,
18};
19
20const MIN_RATIO: u32 = 4;
22const MIN_MATCH_LENGTH: usize = 7;
24const LENGTH_LIMIT: usize = 64;
26const DICT_LIST_SIZE_DEFAULT: usize = 10_000;
28const NOISE_LENGTH: usize = 32;
31const MAX_SAMPLES_SIZE: usize = 2000 << 20;
34const CONTENT_SIZE_MIN: usize = 128;
36pub(crate) const MIN_SAMPLES_SIZE: usize = CONTENT_SIZE_MIN * MIN_RATIO as usize;
38pub const DEFAULT_SELECTIVITY: u32 = 9;
40
41pub(crate) const DICT_SIZE_MIN: usize = super::MIN_TRAINED_DICT_SIZE;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub(crate) enum TooSmall {
47 Dictionary,
49 Corpus,
51 Content,
53}
54
55#[derive(Debug, Clone, Copy, Default)]
59struct DictItem {
60 pos: u32,
61 length: u32,
62 savings: u32,
63}
64
65fn noise_band() -> [u8; NOISE_LENGTH] {
67 let mut band = [0u8; NOISE_LENGTH];
68 let mut acc: u32 = 2_654_435_761;
69 for byte in &mut band {
70 acc = acc.wrapping_mul(2_246_822_519);
71 *byte = (acc >> 21) as u8;
72 }
73 band
74}
75
76struct Corpus<'a> {
84 samples: &'a [u8],
85 noise: [u8; NOISE_LENGTH],
86 kernel: FastpathKernel,
88}
89
90impl<'a> Corpus<'a> {
91 fn new(samples: &'a [u8]) -> Self {
92 Self {
93 samples,
94 noise: noise_band(),
95 kernel: select_kernel(),
96 }
97 }
98
99 #[inline]
101 fn len(&self) -> usize {
102 self.samples.len() + NOISE_LENGTH
103 }
104
105 #[inline]
106 fn byte(&self, at: usize) -> Option<u8> {
107 match self.samples.get(at) {
108 Some(&byte) => Some(byte),
109 None => self.noise.get(at - self.samples.len()).copied(),
110 }
111 }
112
113 #[inline(always)]
115 fn read<const N: usize>(&self, at: usize) -> Option<[u8; N]> {
116 if let Some(bytes) = self.samples.get(at..at + N) {
117 return Some(bytes.try_into().expect("N bytes"));
118 }
119 self.read_across(at)
120 }
121
122 #[cold]
125 #[inline(never)]
126 fn read_across<const N: usize>(&self, at: usize) -> Option<[u8; N]> {
127 let mut out = [0u8; N];
128 for (i, slot) in out.iter_mut().enumerate() {
129 *slot = self.byte(at + i)?;
130 }
131 Some(out)
132 }
133
134 #[inline]
135 fn read16(&self, at: usize) -> Option<u16> {
136 self.read::<2>(at).map(u16::from_le_bytes)
137 }
138
139 #[inline]
140 fn read64(&self, at: usize) -> Option<u64> {
141 self.read::<8>(at).map(u64::from_le_bytes)
142 }
143
144 #[inline]
149 fn common(&self, a: usize, b: usize) -> usize {
150 let samples = self.samples;
151 debug_assert!(a.max(b) <= samples.len());
155 let in_samples = samples.len() - a.max(b);
156 let base = samples.as_ptr();
157 let n = unsafe {
160 dispatch_common_prefix_len_ptr_with_kernel(
161 self.kernel,
162 base.add(a),
163 base.add(b),
164 in_samples,
165 )
166 };
167 if n < in_samples {
168 return n;
169 }
170 self.common_tail(a, b, n)
171 }
172
173 #[cold]
176 #[inline(never)]
177 fn common_tail(&self, a: usize, b: usize, mut n: usize) -> usize {
178 let Some(limit) = self.len().checked_sub(a.max(b)) else {
179 return 0;
180 };
181 while n < limit && self.byte(a + n) == self.byte(b + n) {
182 n += 1;
183 }
184 n
185 }
186
187 fn copy_to(&self, from: usize, out: &mut [u8]) {
189 if let Some(bytes) = self.samples.get(from..from + out.len()) {
190 out.copy_from_slice(bytes);
191 return;
192 }
193 for (i, slot) in out.iter_mut().enumerate() {
194 *slot = self
195 .byte(from + i)
196 .expect("a segment lies inside the corpus");
197 }
198 }
199}
200
201const FAR_AHEAD: usize = 16;
204
205const NEAR_AHEAD: usize = 8;
208
209struct Suffixes {
214 sa: Vec<u32>,
215 noise: usize,
216}
217
218impl Suffixes {
219 fn new(sa: Vec<u32>, len: usize) -> Self {
220 Self { sa, noise: len }
221 }
222
223 #[inline]
225 fn ranks(&self) -> i64 {
226 self.sa.len() as i64
227 }
228
229 #[inline]
233 fn at(&self, at: i64) -> usize {
234 match self.sa.get(at as u64 as usize) {
235 Some(&pos) => pos as usize,
236 None => self.noise,
237 }
238 }
239
240 #[inline(always)]
247 fn prefetch_for(&self, rank: &[u32], samples: &[u8], cursor: usize) {
248 use crate::decoding::prefetch::prefetch_l1_at;
249 let len = self.sa.len();
250 if let Some(&far) = rank.get(cursor + FAR_AHEAD) {
251 prefetch_l1_at(self.sa.as_ptr().wrapping_add(far as usize).cast());
252 }
253 if let Some(&near) = rank.get(cursor + NEAR_AHEAD) {
254 let near = near as usize;
255 let below = near.wrapping_sub(1).min(len - 1);
257 let above = (near + 1).min(len - 1);
258 let (below, above) = unsafe {
260 (
261 *self.sa.get_unchecked(below) as usize,
262 *self.sa.get_unchecked(above) as usize,
263 )
264 };
265 prefetch_l1_at(samples.as_ptr().wrapping_add(below));
266 prefetch_l1_at(samples.as_ptr().wrapping_add(above));
267 }
268 }
269}
270
271pub(crate) fn train_legacy_raw(
278 samples: &[u8],
279 sample_sizes: &[usize],
280 dict_size: usize,
281 selectivity: u32,
282) -> Result<Vec<u8>, TooSmall> {
283 let total: usize = sample_sizes.iter().sum();
284 debug_assert_eq!(total, samples.len(), "the sizes describe the corpus");
285 if total < MIN_SAMPLES_SIZE {
288 return Err(TooSmall::Corpus);
289 }
290 if dict_size < DICT_SIZE_MIN {
291 return Err(TooSmall::Dictionary);
292 }
293 let selectivity = if selectivity == 0 {
294 DEFAULT_SELECTIVITY
295 } else {
296 selectivity
297 };
298 let nb_samples = sample_sizes.len();
299 let min_rep = if selectivity > 30 {
300 MIN_RATIO
301 } else {
302 u32::try_from(nb_samples >> selectivity).map_err(|_| TooSmall::Content)?
305 };
306 let list_size = DICT_LIST_SIZE_DEFAULT.max(nb_samples).max(dict_size / 16);
307 let mut list = vec![DictItem::default(); list_size];
308 list[0] = DictItem {
309 pos: 1,
310 length: 0,
311 savings: u32::MAX,
312 };
313
314 let mut used = total;
316 let mut kept = nb_samples;
317 while used > MAX_SAMPLES_SIZE {
318 kept -= 1;
319 used -= sample_sizes[kept];
320 }
321 let corpus = Corpus::new(&samples[..used]);
322 find_segments(&mut list, &corpus, used, min_rep);
323
324 let segments = list[0].pos as usize;
325 let content_size: usize = list[1..segments].iter().map(|d| d.length as usize).sum();
326 if content_size < CONTENT_SIZE_MIN {
327 return Err(TooSmall::Content);
328 }
329 let mut fit = 1;
331 let mut size = 0usize;
332 while fit < segments {
333 let next = size + list[fit].length as usize;
334 if next > dict_size {
335 break;
336 }
337 size = next;
338 fit += 1;
339 }
340 let mut content = vec![0u8; size];
342 let mut end = size;
343 for item in &list[1..fit] {
344 let length = item.length as usize;
345 let start = end - length;
346 let from = item.pos as usize;
347 corpus.copy_to(from, &mut content[start..end]);
348 end = start;
349 }
350 debug_assert_eq!(end, 0, "the kept segments fill the content exactly");
351 Ok(content)
352}
353
354fn find_segments(list: &mut [DictItem], corpus: &Corpus<'_>, len: usize, min_rep: u32) {
357 let min_ratio = min_rep.max(MIN_RATIO);
358 debug_assert_eq!(corpus.samples.len(), len);
359 let sa = suffix_array(corpus.samples);
360 let mut rank = vec![0u32; len];
361 for (at, &pos) in sa.iter().enumerate() {
362 rank[pos as usize] = at as u32;
363 }
364 let suffixes = Suffixes::new(sa, len);
365 let mut done = vec![false; len + 16];
368 let max_size = list.len() as u32;
369
370 let mut cursor = 0usize;
371 while cursor < len {
372 suffixes.prefetch_for(&rank, corpus.samples, cursor);
373 if done[cursor] {
374 cursor += 1;
375 continue;
376 }
377 let solution = analyze_position(
378 &mut done,
379 &suffixes,
380 i64::from(rank[cursor]),
381 corpus,
382 min_ratio,
383 );
384 if solution.length == 0 {
385 cursor += 1;
386 continue;
387 }
388 insert_item(list, max_size, solution, corpus);
389 cursor += solution.length as usize;
390 }
391}
392
393#[inline]
395fn mark(done: &mut [bool], at: usize) {
396 if let Some(slot) = done.get_mut(at) {
397 *slot = true;
398 }
399}
400
401#[inline]
404fn mark_run(done: &mut [bool], at: usize, len: usize) {
405 let end = (at + len).min(done.len());
406 if at < end {
407 done[at..end].fill(true);
408 }
409}
410
411fn analyze_position(
414 done: &mut [bool],
415 suffixes: &Suffixes,
416 mut start: i64,
417 corpus: &Corpus<'_>,
418 min_ratio: u32,
419) -> DictItem {
420 let mut pos = suffixes.at(start);
421 let mut end = start;
422 let empty = DictItem::default();
423 mark(done, pos);
424
425 let repeats = |a: usize, b: usize| matches!((corpus.read16(a), corpus.read16(b)), (Some(x), Some(y)) if x == y);
428 if repeats(pos, pos + 2) || repeats(pos + 1, pos + 3) || repeats(pos + 2, pos + 4) {
429 let pattern = corpus.read16(pos + 4);
430 let mut pattern_end = 6usize;
431 while pattern.is_some() && corpus.read16(pos + pattern_end) == pattern {
432 pattern_end += 2;
433 }
434 if corpus.byte(pos + pattern_end).is_some()
435 && corpus.byte(pos + pattern_end) == corpus.byte(pos + pattern_end - 1)
436 {
437 pattern_end += 1;
438 }
439 mark_run(done, pos + 1, pattern_end - 1);
440 return empty;
441 }
442
443 loop {
450 end += 1;
451 if end >= suffixes.ranks() || corpus.common(pos, suffixes.at(end)) < MIN_MATCH_LENGTH {
452 break;
453 }
454 }
455 while start > 0 && corpus.common(pos, suffixes.at(start - 1)) >= MIN_MATCH_LENGTH {
456 start -= 1;
457 }
458
459 if ((end - start) as u32) < min_ratio {
460 for id in start..end {
461 mark(done, suffixes.at(id));
462 }
463 return empty;
464 }
465
466 let mut refined_start = start;
469 let mut refined_end = end;
470 let mut mml = MIN_MATCH_LENGTH;
471 loop {
472 let mut current_char = 0u8;
473 let mut current_count = 0u32;
474 let mut current_id = refined_start;
475 let mut selected_count = 0u32;
476 let mut selected_id = current_id;
477 for id in refined_start..refined_end {
478 let c = corpus.byte(suffixes.at(id) + mml).unwrap_or(0);
479 if c != current_char {
480 if current_count > selected_count {
481 selected_count = current_count;
482 selected_id = current_id;
483 }
484 current_id = id;
485 current_char = c;
486 current_count = 0;
487 }
488 current_count += 1;
489 }
490 if current_count > selected_count {
491 selected_count = current_count;
492 selected_id = current_id;
493 }
494 if selected_count < min_ratio {
495 break;
496 }
497 refined_start = selected_id;
498 refined_end = refined_start + i64::from(selected_count);
499 mml += 1;
500 }
501
502 start = refined_start;
504 pos = suffixes.at(refined_start);
505 end = start;
506 let mut lengths = [0u32; LENGTH_LIMIT];
507 loop {
508 end += 1;
509 let length = corpus.common(pos, suffixes.at(end)).min(LENGTH_LIMIT - 1);
510 lengths[length] += 1;
511 if length < MIN_MATCH_LENGTH || end >= suffixes.ranks() {
514 break;
515 }
516 }
517 {
518 let mut length = MIN_MATCH_LENGTH;
519 while length >= MIN_MATCH_LENGTH && start > 0 {
520 length = corpus
521 .common(pos, suffixes.at(start - 1))
522 .min(LENGTH_LIMIT - 1);
523 lengths[length] += 1;
524 if length >= MIN_MATCH_LENGTH {
525 start -= 1;
526 }
527 }
528 }
529
530 let mut cumulative = [0u32; LENGTH_LIMIT];
532 cumulative[LENGTH_LIMIT - 1] = lengths[LENGTH_LIMIT - 1];
533 for i in (0..LENGTH_LIMIT - 1).rev() {
534 cumulative[i] = cumulative[i + 1] + lengths[i];
535 }
536 let mut max_length = LENGTH_LIMIT - 1;
537 while max_length >= MIN_MATCH_LENGTH && cumulative[max_length] < min_ratio {
538 max_length -= 1;
539 }
540 {
542 let last = corpus.byte(pos + max_length - 1);
543 let mut l = max_length;
544 while l >= 2 && corpus.byte(pos + l - 2) == last {
545 l -= 1;
546 }
547 max_length = l;
548 }
549 if max_length < MIN_MATCH_LENGTH {
550 return empty;
551 }
552
553 let mut savings = [0u32; LENGTH_LIMIT];
554 for i in MIN_MATCH_LENGTH..=max_length {
555 savings[i] = savings[i - 1].wrapping_add(lengths[i].wrapping_mul((i - 3) as u32));
556 }
557 let solution = DictItem {
558 pos: pos as u32,
559 length: max_length as u32,
560 savings: savings[max_length],
561 };
562
563 for id in start..end {
564 let tested = suffixes.at(id);
565 let length = if tested == pos {
566 max_length
567 } else {
568 corpus.common(pos, tested).min(max_length)
569 };
570 mark_run(done, tested, length);
571 }
572 solution
573}
574
575fn is_included(corpus: &Corpus<'_>, a: usize, b: usize, length: usize) -> bool {
579 let samples = corpus.samples;
580 if let (Some(x), Some(y)) = (samples.get(a..a + length), samples.get(b..b + length)) {
581 return x == y;
582 }
583 a.max(b) + length <= corpus.len()
584 && (0..length).all(|i| corpus.byte(a + i) == corpus.byte(b + i))
585}
586
587fn promote(list: &mut [DictItem], mut at: usize) -> usize {
589 let item = list[at];
590 while at > 1 && list[at - 1].savings < item.savings {
591 list[at] = list[at - 1];
592 at -= 1;
593 }
594 list[at] = item;
595 at
596}
597
598fn try_merge(list: &mut [DictItem], elt: DictItem, skip: usize, corpus: &Corpus<'_>) -> usize {
601 let size = list[0].pos as usize;
602 let elt_end = elt.pos + elt.length;
603
604 let mut hit = 0;
607 for (u, item) in list[..size].iter().enumerate().skip(1) {
608 if item.pos > elt.pos && item.pos <= elt_end && u != skip {
609 hit = u;
610 break;
611 }
612 }
613 if hit != 0 {
614 let item = &mut list[hit];
615 let added = item.pos - elt.pos;
616 item.length += added;
617 item.pos = elt.pos;
618 item.savings = item
619 .savings
620 .wrapping_add(elt.savings.wrapping_mul(added) / elt.length);
621 item.savings = item.savings.wrapping_add(elt.length / 8);
622 return promote(list, hit);
623 }
624
625 let elt_head = corpus.read64(elt.pos as usize + 1);
629 for u in 1..size {
630 let item = list[u];
631 if u == skip {
632 continue;
633 }
634 if item.pos + item.length >= elt.pos && item.pos < elt.pos {
635 let added = elt_end as i64 - i64::from(item.pos + item.length);
636 let item = &mut list[u];
637 item.savings = item.savings.wrapping_add(elt.length / 8);
638 if added > 0 {
639 item.length += added as u32;
640 item.savings = item
641 .savings
642 .wrapping_add(elt.savings.wrapping_mul(added as u32) / elt.length);
643 }
644 return promote(list, u);
645 }
646 if elt_head.is_some()
647 && corpus.read64(item.pos as usize) == elt_head
648 && is_included(
649 corpus,
650 item.pos as usize,
651 elt.pos as usize + 1,
652 item.length as usize,
653 )
654 {
655 let added = (i64::from(elt.length) - i64::from(item.length)).max(1) as u64;
658 let item = &mut list[u];
659 item.pos = elt.pos;
660 item.savings = item
661 .savings
662 .wrapping_add((u64::from(elt.savings) * added / u64::from(elt.length)) as u32);
663 item.length = elt.length.min(item.length + 1);
664 return u;
665 }
666 }
667 0
668}
669
670fn remove_item(list: &mut [DictItem], id: usize) {
672 if id == 0 {
673 return;
674 }
675 let max = list[0].pos as usize;
676 for u in id..max - 1 {
677 list[u] = list[u + 1];
678 }
679 list[0].pos -= 1;
680}
681
682fn insert_item(list: &mut [DictItem], max_size: u32, elt: DictItem, corpus: &Corpus<'_>) {
686 let mut merge_id = try_merge(list, elt, 0, corpus);
687 if merge_id != 0 {
688 loop {
689 let merged = try_merge(list, list[merge_id], merge_id, corpus);
690 if merged == 0 {
691 return;
692 }
693 remove_item(list, merge_id);
694 merge_id = merged;
695 }
696 }
697 let next = list[0].pos.min(max_size - 1) as usize;
698 let mut current = next - 1;
699 while list[current].savings < elt.savings {
700 list[current + 1] = list[current];
701 current -= 1;
702 }
703 list[current + 1] = elt;
704 list[0].pos = next as u32 + 1;
705}
706
707#[cfg(test)]
708mod tests;