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 = 256;
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 dict_size < DICT_SIZE_MIN {
286 return Err(TooSmall::Dictionary);
287 }
288 if total < MIN_SAMPLES_SIZE {
289 return Err(TooSmall::Corpus);
290 }
291 let selectivity = if selectivity == 0 {
292 DEFAULT_SELECTIVITY
293 } else {
294 selectivity
295 };
296 let nb_samples = sample_sizes.len();
297 let min_rep = if selectivity > 30 {
298 MIN_RATIO
299 } else {
300 u32::try_from(nb_samples >> selectivity).map_err(|_| TooSmall::Content)?
303 };
304 let list_size = DICT_LIST_SIZE_DEFAULT.max(nb_samples).max(dict_size / 16);
305 let mut list = vec![DictItem::default(); list_size];
306 list[0] = DictItem {
307 pos: 1,
308 length: 0,
309 savings: u32::MAX,
310 };
311
312 let mut used = total;
314 let mut kept = nb_samples;
315 while used > MAX_SAMPLES_SIZE {
316 kept -= 1;
317 used -= sample_sizes[kept];
318 }
319 let corpus = Corpus::new(&samples[..used]);
320 find_segments(&mut list, &corpus, used, min_rep);
321
322 let segments = list[0].pos as usize;
323 let content_size: usize = list[1..segments].iter().map(|d| d.length as usize).sum();
324 if content_size < CONTENT_SIZE_MIN {
325 return Err(TooSmall::Content);
326 }
327 let mut fit = 1;
329 let mut size = 0usize;
330 while fit < segments {
331 let next = size + list[fit].length as usize;
332 if next > dict_size {
333 break;
334 }
335 size = next;
336 fit += 1;
337 }
338 let mut content = vec![0u8; size];
340 let mut end = size;
341 for item in &list[1..fit] {
342 let length = item.length as usize;
343 let start = end - length;
344 let from = item.pos as usize;
345 corpus.copy_to(from, &mut content[start..end]);
346 end = start;
347 }
348 debug_assert_eq!(end, 0, "the kept segments fill the content exactly");
349 Ok(content)
350}
351
352fn find_segments(list: &mut [DictItem], corpus: &Corpus<'_>, len: usize, min_rep: u32) {
355 let min_ratio = min_rep.max(MIN_RATIO);
356 debug_assert_eq!(corpus.samples.len(), len);
357 let sa = suffix_array(corpus.samples);
358 let mut rank = vec![0u32; len];
359 for (at, &pos) in sa.iter().enumerate() {
360 rank[pos as usize] = at as u32;
361 }
362 let suffixes = Suffixes::new(sa, len);
363 let mut done = vec![false; len + 16];
366 let max_size = list.len() as u32;
367
368 let mut cursor = 0usize;
369 while cursor < len {
370 suffixes.prefetch_for(&rank, corpus.samples, cursor);
371 if done[cursor] {
372 cursor += 1;
373 continue;
374 }
375 let solution = analyze_position(
376 &mut done,
377 &suffixes,
378 i64::from(rank[cursor]),
379 corpus,
380 min_ratio,
381 );
382 if solution.length == 0 {
383 cursor += 1;
384 continue;
385 }
386 insert_item(list, max_size, solution, corpus);
387 cursor += solution.length as usize;
388 }
389}
390
391#[inline]
393fn mark(done: &mut [bool], at: usize) {
394 if let Some(slot) = done.get_mut(at) {
395 *slot = true;
396 }
397}
398
399#[inline]
402fn mark_run(done: &mut [bool], at: usize, len: usize) {
403 let end = (at + len).min(done.len());
404 if at < end {
405 done[at..end].fill(true);
406 }
407}
408
409fn analyze_position(
412 done: &mut [bool],
413 suffixes: &Suffixes,
414 mut start: i64,
415 corpus: &Corpus<'_>,
416 min_ratio: u32,
417) -> DictItem {
418 let mut pos = suffixes.at(start);
419 let mut end = start;
420 let empty = DictItem::default();
421 mark(done, pos);
422
423 let repeats = |a: usize, b: usize| matches!((corpus.read16(a), corpus.read16(b)), (Some(x), Some(y)) if x == y);
426 if repeats(pos, pos + 2) || repeats(pos + 1, pos + 3) || repeats(pos + 2, pos + 4) {
427 let pattern = corpus.read16(pos + 4);
428 let mut pattern_end = 6usize;
429 while pattern.is_some() && corpus.read16(pos + pattern_end) == pattern {
430 pattern_end += 2;
431 }
432 if corpus.byte(pos + pattern_end).is_some()
433 && corpus.byte(pos + pattern_end) == corpus.byte(pos + pattern_end - 1)
434 {
435 pattern_end += 1;
436 }
437 mark_run(done, pos + 1, pattern_end - 1);
438 return empty;
439 }
440
441 loop {
448 end += 1;
449 if end >= suffixes.ranks() || corpus.common(pos, suffixes.at(end)) < MIN_MATCH_LENGTH {
450 break;
451 }
452 }
453 while start > 0 && corpus.common(pos, suffixes.at(start - 1)) >= MIN_MATCH_LENGTH {
454 start -= 1;
455 }
456
457 if ((end - start) as u32) < min_ratio {
458 for id in start..end {
459 mark(done, suffixes.at(id));
460 }
461 return empty;
462 }
463
464 let mut refined_start = start;
467 let mut refined_end = end;
468 let mut mml = MIN_MATCH_LENGTH;
469 loop {
470 let mut current_char = 0u8;
471 let mut current_count = 0u32;
472 let mut current_id = refined_start;
473 let mut selected_count = 0u32;
474 let mut selected_id = current_id;
475 for id in refined_start..refined_end {
476 let c = corpus.byte(suffixes.at(id) + mml).unwrap_or(0);
477 if c != current_char {
478 if current_count > selected_count {
479 selected_count = current_count;
480 selected_id = current_id;
481 }
482 current_id = id;
483 current_char = c;
484 current_count = 0;
485 }
486 current_count += 1;
487 }
488 if current_count > selected_count {
489 selected_count = current_count;
490 selected_id = current_id;
491 }
492 if selected_count < min_ratio {
493 break;
494 }
495 refined_start = selected_id;
496 refined_end = refined_start + i64::from(selected_count);
497 mml += 1;
498 }
499
500 start = refined_start;
502 pos = suffixes.at(refined_start);
503 end = start;
504 let mut lengths = [0u32; LENGTH_LIMIT];
505 loop {
506 end += 1;
507 let length = corpus.common(pos, suffixes.at(end)).min(LENGTH_LIMIT - 1);
508 lengths[length] += 1;
509 if length < MIN_MATCH_LENGTH || end >= suffixes.ranks() {
512 break;
513 }
514 }
515 {
516 let mut length = MIN_MATCH_LENGTH;
517 while length >= MIN_MATCH_LENGTH && start > 0 {
518 length = corpus
519 .common(pos, suffixes.at(start - 1))
520 .min(LENGTH_LIMIT - 1);
521 lengths[length] += 1;
522 if length >= MIN_MATCH_LENGTH {
523 start -= 1;
524 }
525 }
526 }
527
528 let mut cumulative = [0u32; LENGTH_LIMIT];
530 cumulative[LENGTH_LIMIT - 1] = lengths[LENGTH_LIMIT - 1];
531 for i in (0..LENGTH_LIMIT - 1).rev() {
532 cumulative[i] = cumulative[i + 1] + lengths[i];
533 }
534 let mut max_length = LENGTH_LIMIT - 1;
535 while max_length >= MIN_MATCH_LENGTH && cumulative[max_length] < min_ratio {
536 max_length -= 1;
537 }
538 {
540 let last = corpus.byte(pos + max_length - 1);
541 let mut l = max_length;
542 while l >= 2 && corpus.byte(pos + l - 2) == last {
543 l -= 1;
544 }
545 max_length = l;
546 }
547 if max_length < MIN_MATCH_LENGTH {
548 return empty;
549 }
550
551 let mut savings = [0u32; LENGTH_LIMIT];
552 for i in MIN_MATCH_LENGTH..=max_length {
553 savings[i] = savings[i - 1].wrapping_add(lengths[i].wrapping_mul((i - 3) as u32));
554 }
555 let solution = DictItem {
556 pos: pos as u32,
557 length: max_length as u32,
558 savings: savings[max_length],
559 };
560
561 for id in start..end {
562 let tested = suffixes.at(id);
563 let length = if tested == pos {
564 max_length
565 } else {
566 corpus.common(pos, tested).min(max_length)
567 };
568 mark_run(done, tested, length);
569 }
570 solution
571}
572
573fn is_included(corpus: &Corpus<'_>, a: usize, b: usize, length: usize) -> bool {
577 let samples = corpus.samples;
578 if let (Some(x), Some(y)) = (samples.get(a..a + length), samples.get(b..b + length)) {
579 return x == y;
580 }
581 a.max(b) + length <= corpus.len()
582 && (0..length).all(|i| corpus.byte(a + i) == corpus.byte(b + i))
583}
584
585fn promote(list: &mut [DictItem], mut at: usize) -> usize {
587 let item = list[at];
588 while at > 1 && list[at - 1].savings < item.savings {
589 list[at] = list[at - 1];
590 at -= 1;
591 }
592 list[at] = item;
593 at
594}
595
596fn try_merge(list: &mut [DictItem], elt: DictItem, skip: usize, corpus: &Corpus<'_>) -> usize {
599 let size = list[0].pos as usize;
600 let elt_end = elt.pos + elt.length;
601
602 let mut hit = 0;
605 for (u, item) in list[..size].iter().enumerate().skip(1) {
606 if item.pos > elt.pos && item.pos <= elt_end && u != skip {
607 hit = u;
608 break;
609 }
610 }
611 if hit != 0 {
612 let item = &mut list[hit];
613 let added = item.pos - elt.pos;
614 item.length += added;
615 item.pos = elt.pos;
616 item.savings = item
617 .savings
618 .wrapping_add(elt.savings.wrapping_mul(added) / elt.length);
619 item.savings = item.savings.wrapping_add(elt.length / 8);
620 return promote(list, hit);
621 }
622
623 let elt_head = corpus.read64(elt.pos as usize + 1);
627 for u in 1..size {
628 let item = list[u];
629 if u == skip {
630 continue;
631 }
632 if item.pos + item.length >= elt.pos && item.pos < elt.pos {
633 let added = elt_end as i64 - i64::from(item.pos + item.length);
634 let item = &mut list[u];
635 item.savings = item.savings.wrapping_add(elt.length / 8);
636 if added > 0 {
637 item.length += added as u32;
638 item.savings = item
639 .savings
640 .wrapping_add(elt.savings.wrapping_mul(added as u32) / elt.length);
641 }
642 return promote(list, u);
643 }
644 if elt_head.is_some()
645 && corpus.read64(item.pos as usize) == elt_head
646 && is_included(
647 corpus,
648 item.pos as usize,
649 elt.pos as usize + 1,
650 item.length as usize,
651 )
652 {
653 let added = (i64::from(elt.length) - i64::from(item.length)).max(1) as u64;
656 let item = &mut list[u];
657 item.pos = elt.pos;
658 item.savings = item
659 .savings
660 .wrapping_add((u64::from(elt.savings) * added / u64::from(elt.length)) as u32);
661 item.length = elt.length.min(item.length + 1);
662 return u;
663 }
664 }
665 0
666}
667
668fn remove_item(list: &mut [DictItem], id: usize) {
670 if id == 0 {
671 return;
672 }
673 let max = list[0].pos as usize;
674 for u in id..max - 1 {
675 list[u] = list[u + 1];
676 }
677 list[0].pos -= 1;
678}
679
680fn insert_item(list: &mut [DictItem], max_size: u32, elt: DictItem, corpus: &Corpus<'_>) {
684 let mut merge_id = try_merge(list, elt, 0, corpus);
685 if merge_id != 0 {
686 loop {
687 let merged = try_merge(list, list[merge_id], merge_id, corpus);
688 if merged == 0 {
689 return;
690 }
691 remove_item(list, merge_id);
692 merge_id = merged;
693 }
694 }
695 let next = list[0].pos.min(max_size - 1) as usize;
696 let mut current = next - 1;
697 while list[current].savings < elt.savings {
698 list[current + 1] = list[current];
699 current -= 1;
700 }
701 list[current + 1] = elt;
702 list[0].pos = next as u32 + 1;
703}
704
705#[cfg(test)]
706mod tests;