//! Garbage collection of long clauses. use partial_ref::{partial, PartialRef}; use crate::{ context::{parts::*, Context}, prop::Reason, }; use super::{ClauseAlloc, Tier}; /// Perform a garbage collection of long clauses if necessary. pub fn collect_garbage( mut ctx: partial!( Context, mut ClauseAllocP, mut ClauseDbP, mut ImplGraphP, mut WatchlistsP, TrailP, ), ) { let alloc = ctx.part(ClauseAllocP); let db = ctx.part(ClauseDbP); // Collecting when a fixed fraction of the allocation is garbage amortizes collection costs. if db.garbage_size * 2 > alloc.buffer_size() { collect_garbage_now(ctx.borrow()); } } /// Unconditionally perform a garbage collection of long clauses. /// /// This needs to invalidate or update any other data structure containing references to /// clauses. fn collect_garbage_now( mut ctx: partial!( Context, mut ClauseAllocP, mut ClauseDbP, mut ImplGraphP, mut WatchlistsP, TrailP, ), ) { ctx.part_mut(WatchlistsP).disable(); mark_asserting_clauses(ctx.borrow()); let (db, mut ctx) = ctx.split_part_mut(ClauseDbP); let (impl_graph, mut ctx) = ctx.split_part_mut(ImplGraphP); let alloc = ctx.part_mut(ClauseAllocP); assert!( db.garbage_size <= alloc.buffer_size(), "Inconsistent garbage tracking in ClauseDb" ); let current_size = alloc.buffer_size() - db.garbage_size; // Allocating just the current size would lead to an immediate growing when new clauses are // learned, overallocating here avoids that. let mut new_alloc = ClauseAlloc::with_capacity(current_size * 2); let mut new_clauses = vec![]; let mut new_by_tier: [Vec<_>; Tier::count()] = Default::default(); // TODO Optimize order of clauses (benchmark this) db.clauses.retain(|&cref| { let clause = alloc.clause(cref); let mut header = *clause.header(); if header.deleted() { false } else { let clause_is_asserting = header.mark(); header.set_mark(false); let new_cref = new_alloc.add_clause(header, clause.lits()); new_clauses.push(new_cref); new_by_tier[header.tier() as usize].push(new_cref); if clause_is_asserting { let asserted_lit = clause.lits()[0]; debug_assert_eq!(impl_graph.reason(asserted_lit.var()), &Reason::Long(cref)); impl_graph.update_reason(asserted_lit.var(), Reason::Long(new_cref)); } true } }); *ctx.part_mut(ClauseAllocP) = new_alloc; db.clauses = new_clauses; db.by_tier = new_by_tier; db.garbage_size = 0; } /// Mark asserting clauses to track them through GC. fn mark_asserting_clauses(mut ctx: partial!(Context, mut ClauseAllocP, ImplGraphP, TrailP,)) { let (trail, mut ctx) = ctx.split_part(TrailP); let (alloc, ctx) = ctx.split_part_mut(ClauseAllocP); let impl_graph = ctx.part(ImplGraphP); for &lit in trail.trail().iter() { if let Reason::Long(cref) = impl_graph.reason(lit.var()) { alloc.header_mut(*cref).set_mark(true); } } } #[cfg(test)] mod tests { use super::*; use std::cmp::max; use partial_ref::IntoPartialRefMut; use proptest::*; use varisat_formula::{cnf::strategy::*, Lit}; use crate::{ clause::{db, ClauseHeader}, context::set_var_count, prop::enqueue_assignment, }; proptest! { #[test] fn garbage_collection( input_a in cnf_formula(2..100usize, 500..1000, 3..30), input_b in cnf_formula(2..100usize, 10..500, 4..20), ) { let mut ctx = Context::default(); let mut ctx = ctx.into_partial_ref_mut(); set_var_count(ctx.borrow(), max(input_a.var_count(), input_b.var_count())); let mut crefs_a = vec![]; let mut crefs_b = vec![]; for lits in input_a.iter() { let header = ClauseHeader::new(); let cref = db::add_clause(ctx.borrow(), header, lits); crefs_a.push(cref); } for lits in input_b.iter() { let header = ClauseHeader::new(); let cref = db::add_clause(ctx.borrow(), header, lits); crefs_b.push(cref); if ctx.part(AssignmentP).lit_value(lits[0]) == None { // This isn't consistent, as the clause isn't actually propagating, but that // isn't checked during garbage collection enqueue_assignment(ctx.borrow(), lits[0], Reason::Long(cref)); } } for cref in crefs_a { db::delete_clause(ctx.borrow(), cref); prop_assert!(ctx.part(ClauseDbP).garbage_size > 0); } let old_buffer_size = ctx.part(ClauseAllocP).buffer_size(); collect_garbage(ctx.borrow()); prop_assert!( ctx.part(ClauseDbP).garbage_size * 2 < ctx.part(ClauseAllocP).buffer_size() ); prop_assert!( old_buffer_size > ctx.part(ClauseAllocP).buffer_size() ); prop_assert!(!ctx.part(WatchlistsP).enabled()); let mut output_clauses: Vec> = vec![]; for &cref in ctx.part(ClauseDbP).clauses.iter() { let clause = ctx.part(ClauseAllocP).clause(cref); if clause.header().deleted() { continue; } prop_assert!(!clause.header().mark()); output_clauses.push(clause.lits().to_vec()); } let mut input_clauses: Vec> = input_b .iter() .map(|c| c.to_vec()) .collect(); output_clauses.sort(); input_clauses.sort(); prop_assert_eq!(input_clauses, output_clauses); for &lit in ctx.part(TrailP).trail() { if let Reason::Long(cref) = ctx.part(ImplGraphP).reason(lit.var()) { prop_assert_eq!(ctx.part(ClauseAllocP).clause(*cref).lits()[0], lit) } } } } }