Decide which subterms to share based on profitability. Sharing is profitable when: `(N - 1) * term_size > N * share_ref_size` where N is usage count, term_size is effective size, and share_ref_size is the size of a Share(idx) reference at the current index. Optimized from O(k×n) to O(n log n) by pre-sorting candidates.
( info_map: &HashMap<blake3::Hash, SubtermInfo>, topo_order: &[blake3::Hash], )
| 576 | /// Optimized from O(k×n) to O(n log n) by pre-sorting candidates. |
| 577 | pub fn decide_sharing( |
| 578 | info_map: &FxHashMap<blake3::Hash, SubtermInfo>, |
| 579 | topo_order: &[blake3::Hash], |
| 580 | ) -> IndexSet<blake3::Hash> { |
| 581 | let effective_sizes = compute_effective_sizes(info_map, topo_order); |
| 582 | |
| 583 | // Pre-filter and sort candidates by potential savings (assuming minimal ref_size=1) |
| 584 | // This gives us a stable ordering since relative savings don't change as ref_size grows |
| 585 | let mut candidates: Vec<_> = info_map |
| 586 | .iter() |
| 587 | .filter(|(_, info)| info.usage_count >= 2) |
| 588 | .filter_map(|(hash, info)| { |
| 589 | let term_size = *effective_sizes.get(hash)?; |
| 590 | let n = info.usage_count; |
| 591 | // Potential savings assuming ref_size = 1 (minimum) |
| 592 | let potential = (n as isize - 1) * (term_size as isize) - (n as isize); |
| 593 | if potential > 0 { Some((*hash, term_size, n)) } else { None } |
| 594 | }) |
| 595 | .collect(); |
| 596 | |
| 597 | // Sort by decreasing gross benefit, with hash bytes as tie-breaker for determinism |
| 598 | candidates.sort_unstable_by(|a, b| { |
| 599 | let gross_a = (a.2 as isize - 1) * (a.1 as isize); |
| 600 | let gross_b = (b.2 as isize - 1) * (b.1 as isize); |
| 601 | match gross_b.cmp(&gross_a) { |
| 602 | std::cmp::Ordering::Equal => a.0.as_bytes().cmp(b.0.as_bytes()), |
| 603 | other => other, |
| 604 | } |
| 605 | }); |
| 606 | |
| 607 | let mut shared: IndexSet<blake3::Hash> = IndexSet::new(); |
| 608 | |
| 609 | // Process ALL candidates - don't break early! |
| 610 | // The early-break was incorrect: ref_size growth affects candidates differently |
| 611 | // based on their usage count. A high-usage small term may become unprofitable |
| 612 | // while a low-usage large term remains profitable. |
| 613 | for (hash, term_size, usage_count) in candidates { |
| 614 | let next_idx = shared.len(); |
| 615 | let next_ref_size = |
| 616 | Tag4::new(Expr::FLAG_SHARE, next_idx as u64).encoded_size(); |
| 617 | let n = usage_count as isize; |
| 618 | let savings = (n - 1) * (term_size as isize) - n * (next_ref_size as isize); |
| 619 | |
| 620 | if savings > 0 { |
| 621 | shared.insert(hash); |
| 622 | } |
| 623 | } |
| 624 | |
| 625 | shared |
| 626 | } |
| 627 | |
| 628 | /// Rewrite expressions to use Share(idx) references for shared subterms. |
| 629 | /// |
| 630 | /// Returns the rewritten expressions and the sharing vector. |