From 37e29a352fb55187ac504b9df264fde9ca4af713 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 25 Sep 2023 16:55:13 -0700 Subject: [PATCH 1/2] Add benchmarking --- Cargo.toml | 5 ++ benches/throughput.rs | 181 ++++++++++++++++++++++++++++++++++++++++++ rust-toolchain.toml | 2 + 3 files changed, 188 insertions(+) create mode 100644 benches/throughput.rs create mode 100644 rust-toolchain.toml diff --git a/Cargo.toml b/Cargo.toml index 739885b..af62ce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,8 @@ crossbeam-utils = "^0.8" trybuild = "^1.0" rayon = "^1.5" rand = "0.8" + +criterion = "0.5.1" +[[bench]] +name = "throughput" +harness = false diff --git a/benches/throughput.rs b/benches/throughput.rs new file mode 100644 index 0000000..a1186a7 --- /dev/null +++ b/benches/throughput.rs @@ -0,0 +1,181 @@ +use hashconsing::*; +use rand::distributions::Distribution; + +type Term = HConsed; + +const TERM_SIZE: usize = 10_000; + +#[derive(Hash, Clone, PartialEq, Eq, Debug)] +enum Op +{ + Var(usize), + Lam, + App, +} + +impl Op +{ + fn arity(&self) -> usize + { + match self { + Op::Var(_) => 0, + Op::Lam => 1, + Op::App => 2, + } + } +} + +#[derive(Hash, Clone, PartialEq, Eq, Debug)] +struct ActualTerm +{ + op: Op, + children: Vec, +} + +impl ActualTerm +{ + fn new(op: Op, children: Vec) -> ActualTerm + { + ActualTerm { op, children } + } +} + +/// A distribution of n usizes that sum to this value. +/// (n, sum) +pub struct Sum(usize, usize); +impl rand::distributions::Distribution> for Sum +{ + fn sample(&self, rng: &mut R) -> Vec + { + use rand::seq::SliceRandom; + let mut acc = self.1; + let mut ns = Vec::new(); + assert!(acc == 0 || self.0 > 0); + while acc > 0 && ns.len() < self.0 { + let x = rng.gen_range(0..acc); + acc -= x; + ns.push(x); + } + while ns.len() < self.0 { + ns.push(0); + } + if acc > 0 { + *ns.last_mut().unwrap() += acc; + } + ns.shuffle(rng); + ns + } +} + +consign! { + /// Factory for terms. + let TERMS_ARC = consign(TERM_SIZE) for ActualTerm ; +} + +pub struct TermDist +{ + factory: &'static std::sync::RwLock>, + subterm_count: usize, + binding_depth: usize, +} + +impl rand::distributions::Distribution for TermDist +{ + fn sample(&self, rng: &mut R) -> Term + { + use rand::seq::SliceRandom; + let ops = &[ + Op::Var(rng.gen_range(0..self.binding_depth)), + Op::Lam, + Op::App, + ]; + let o = match self.subterm_count { + 1 => ops[..1].choose(rng), // arity 0 + 2 => ops[1..2].choose(rng), // arity 1 + _ => ops[1..].choose(rng), // others + } + .unwrap() + .clone(); + // Now, self.0 is a least arity+1 + let excess = self.subterm_count - 1 - o.arity(); + let ns = Sum(o.arity(), excess).sample(rng); + let subterms = ns + .into_iter() + .map(|n| { + TermDist { + factory: self.factory.clone(), + subterm_count: n + 1, + binding_depth: if o == Op::Lam { + self.binding_depth + 1 + } else { + self.binding_depth + }, + } + .sample(rng) + }) + .collect::>(); + self.factory.mk(ActualTerm::new(o, subterms)) + } +} + +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +pub fn criterion_benchmark(c: &mut Criterion) +{ + let batch_size = 10; + let mut group = c.benchmark_group("Benchmark"); + for i in (100..=1001).step_by(100) { + group.bench_with_input( + BenchmarkId::new("Arc (System Allocator)", i), + &i, + move |b, n| { + let n = *n; + + b.iter_custom(move |iters| { + let factory = &TERMS_ARC; + let rng = &mut rand::thread_rng(); + let mut duration = Duration::new(0, 0); + + let dist = TermDist { + factory, + subterm_count: 10, + binding_depth: 1, + }; + for _ in 0..iters { + let mut vec = VecDeque::with_capacity(n); + for _ in 0..n { + vec.push_back(dist.sample(rng)); + } + + let start = Instant::now(); + for _ in 0..n { + for _ in 0..batch_size { + vec.pop_front(); + } + for _ in 0..batch_size { + vec.push_back(dist.sample(rng)); + } + factory.shrink_to_fit(); + } + duration += start.elapsed() + } + factory.shrink_to_fit(); + + duration + }); + }, + ); + } + group.finish(); +} + +criterion_group!( + name = benches; + config = Criterion::default().with_plots().sample_size(50); + targets = criterion_benchmark +); +criterion_main!(benches); diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" From 8c663871d00e1fbf90947792e074dac68b59d23d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 25 Sep 2023 18:07:12 -0700 Subject: [PATCH 2/2] Add weak and strong pointer traits --- src/alloc.rs | 74 ++++++++++++++++++++++++++++++++ src/lib.rs | 118 ++++++++++++++++++++++++++++----------------------- 2 files changed, 140 insertions(+), 52 deletions(-) create mode 100644 src/alloc.rs diff --git a/src/alloc.rs b/src/alloc.rs new file mode 100644 index 0000000..089a08a --- /dev/null +++ b/src/alloc.rs @@ -0,0 +1,74 @@ + +pub trait Arc: std::ops::Deref + core::borrow::Borrow + Clone +{ + fn downgrade(&self) -> W; + fn strong_count(&self) -> usize; +} +pub trait Weak: Clone +{ + fn upgrade(&self) -> Option; + fn strong_count(&self) -> usize; +} + +impl Arc> for std::sync::Arc +{ + fn downgrade(&self) -> std::sync::Weak + { + std::sync::Arc::downgrade(&self) + } + fn strong_count(&self) -> usize { + std::sync::Arc::strong_count(&self) + } +} +impl Weak> for std::sync::Weak +{ + fn upgrade(&self) -> Option> + { + std::sync::Weak::upgrade(&self) + } + fn strong_count(&self) -> usize { + std::sync::Weak::strong_count(&self) + } +} + + +pub trait Allocator: Default +{ + /// Strong `std::sync::Arc`-like pointer + type Strong: Arc; + + /// `std::sync::Weak`-like pointer + type Weak: Weak; + + fn alloc(&self, data: T) -> Self::Strong; +} + +#[derive(Default)] +pub struct DefaultAllocator {} + +impl Allocator for DefaultAllocator +{ + type Strong = std::sync::Arc; + type Weak = std::sync::Weak; + + fn alloc(&self, data: T) -> Self::Strong + { + Self::Strong::new(data) + } +} + +/* +#[cfg(feature = "shared_arena")] +use shared_arena::ArenaArc; + +#[cfg(feature = "shared_arena")] +impl Allocator for shared_arena::SharedArena +{ + type P = ArenaArc; + + fn alloc(data: T) -> Self::P + { + self.alloc_arc(data) + } +} +*/ diff --git a/src/lib.rs b/src/lib.rs index c00789c..70ec661 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -221,11 +221,13 @@ use std::{ fmt, hash::{BuildHasher, Hash, Hasher}, ops::Deref, - sync::{Arc, RwLock, Weak}, + sync::RwLock, }; pub extern crate lazy_static; +mod alloc; +use alloc::Allocator; #[cfg(test)] mod test; @@ -288,17 +290,17 @@ pub trait HashConsed { } /// A hashconsed value. -pub struct HConsed { +pub struct HConsed=alloc::DefaultAllocator> { /// The actual element. - elm: Arc, + elm: Alloc::Strong, /// Unique identifier of the element. uid: u64, } -impl HashConsed for HConsed { +impl> HashConsed for HConsed { type Inner = T; } -impl HConsed { +impl> HConsed { /// The inner element. Can also be accessed *via* dereferencing. #[inline] pub fn get(&self) -> &T { @@ -311,31 +313,34 @@ impl HConsed { } /// Turns a hashconsed thing in a weak hashconsed thing. #[inline] - pub fn to_weak(&self) -> WHConsed { + pub fn to_weak(&self) -> WHConsed { + use alloc::Arc; WHConsed { - elm: Arc::downgrade(&self.elm), + elm: self.elm.downgrade(), uid: self.uid, } } /// Weak reference version. - pub fn to_weak_ref(&self) -> Weak { - Arc::downgrade(&self.elm) + pub fn to_weak_ref(&self) -> A::Weak { + use alloc::Arc; + self.elm.downgrade() } /// Number of (strong) references to this term. pub fn arc_count(&self) -> usize { - Arc::strong_count(&self.elm) + use alloc::Arc; + self.elm.strong_count() } } -impl fmt::Debug for HConsed { +impl> fmt::Debug for HConsed { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - write!(fmt, "{:?}", self.elm) + write!(fmt, "{:?}", self.elm.deref()) } } -impl Clone for HConsed { +impl> Clone for HConsed { fn clone(&self) -> Self { HConsed { elm: self.elm.clone(), @@ -344,26 +349,26 @@ impl Clone for HConsed { } } -impl PartialEq for HConsed { +impl> PartialEq for HConsed { #[inline] fn eq(&self, rhs: &Self) -> bool { self.uid == rhs.uid } } -impl Eq for HConsed {} -impl PartialOrd for HConsed { +impl> Eq for HConsed {} +impl> PartialOrd for HConsed { #[inline] fn partial_cmp(&self, other: &Self) -> Option { self.uid.partial_cmp(&other.uid) } } -impl Ord for HConsed { +impl> Ord for HConsed { #[inline] fn cmp(&self, other: &Self) -> Ordering { self.uid.cmp(&other.uid) } } -impl Hash for HConsed { +impl> Hash for HConsed { #[inline] fn hash(&self, state: &mut H) where @@ -373,7 +378,7 @@ impl Hash for HConsed { } } -impl Deref for HConsed { +impl> Deref for HConsed { type Target = T; #[inline] fn deref(&self) -> &T { @@ -381,7 +386,7 @@ impl Deref for HConsed { } } -impl fmt::Display for HConsed { +impl> fmt::Display for HConsed { #[inline] fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { self.elm.fmt(fmt) @@ -389,15 +394,16 @@ impl fmt::Display for HConsed { } /// Weak version of `HConsed`. -pub struct WHConsed { +pub struct WHConsed=alloc::DefaultAllocator> { /// The actual element. - elm: Weak, + elm: Alloc::Weak, /// Unique identifier of the element. uid: u64, } -impl WHConsed { +impl> WHConsed { /// Turns a weak hashconsed thing in a hashconsed thing. - pub fn to_hconsed(&self) -> Option> { + pub fn to_hconsed(&self) -> Option> { + use alloc::Weak; self.elm.upgrade().map(|arc| HConsed { elm: arc, uid: self.uid, @@ -405,14 +411,15 @@ impl WHConsed { } /// A reference to the underlying weak reference. - pub fn as_weak_ref(&self) -> &Weak { + pub fn as_weak_ref(&self) -> &A::Weak { &self.elm } } -impl fmt::Display for WHConsed { +impl> fmt::Display for WHConsed { #[inline] fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + use alloc::Weak; if let Some(arc) = self.elm.upgrade() { arc.fmt(fmt) } else { @@ -421,7 +428,7 @@ impl fmt::Display for WHConsed { } } -impl Hash for WHConsed { +impl> Hash for WHConsed { #[inline] fn hash(&self, state: &mut H) where @@ -431,20 +438,20 @@ impl Hash for WHConsed { } } -impl PartialEq for WHConsed { +impl> PartialEq for WHConsed { #[inline] fn eq(&self, rhs: &Self) -> bool { self.uid == rhs.uid } } -impl Eq for WHConsed {} -impl PartialOrd for WHConsed { +impl> Eq for WHConsed {} +impl> PartialOrd for WHConsed { #[inline] fn partial_cmp(&self, other: &Self) -> Option { self.uid.partial_cmp(&other.uid) } } -impl Ord for WHConsed { +impl> Ord for WHConsed { #[inline] fn cmp(&self, other: &Self) -> Ordering { self.uid.cmp(&other.uid) @@ -452,20 +459,23 @@ impl Ord for WHConsed { } /// The consign storing the actual hash consed elements as `HConsed`s. -pub struct HConsign { +pub struct HConsign=alloc::DefaultAllocator> { /// The actual hash consing table. - table: HashMap, S>, + table: HashMap, S>, /// Counter for uids. count: u64, + + allocator: Alloc, } -impl HConsign { +impl> HConsign { /// Creates an empty consign. #[inline] pub fn empty() -> Self { HConsign { table: HashMap::new(), count: 0, + allocator: Default::default(), } } @@ -475,16 +485,17 @@ impl HConsign { HConsign { table: HashMap::with_capacity(capacity), count: 0, + allocator: Default::default(), } } } -impl HConsign { +impl> HConsign { /// Fold on the elements stored in the consign. #[inline] pub fn fold(&self, mut init: Acc, mut f: F) -> Acc where - F: FnMut(Acc, HConsed) -> Acc, + F: FnMut(Acc, HConsed) -> Acc, { for weak in self.table.values() { if let Some(consed) = weak.to_hconsed() { @@ -498,7 +509,7 @@ impl HConsign { #[inline] pub fn fold_res(&self, mut init: Acc, mut f: F) -> Result where - F: FnMut(Acc, HConsed) -> Result, + F: FnMut(Acc, HConsed) -> Result, { for weak in self.table.values() { if let Some(consed) = weak.to_hconsed() { @@ -526,13 +537,14 @@ impl HConsign { } } -impl HConsign { +impl> HConsign { /// Creates an empty consign with a custom hash #[inline] pub fn with_hasher(build_hasher: S) -> Self { HConsign { table: HashMap::with_hasher(build_hasher), count: 0, + allocator: Default::default(), } } @@ -542,6 +554,7 @@ impl HConsign { HConsign { table: HashMap::with_capacity_and_hasher(capacity, build_hasher), count: 0, + allocator: Default::default(), } } @@ -554,7 +567,7 @@ impl HConsign { /// /// This is checked in `debug` but not `release`. #[inline] - fn insert(&mut self, key: T, wconsed: WHConsed) { + fn insert(&mut self, key: T, wconsed: WHConsed) { let prev = self.table.insert(key, wconsed); debug_assert!(match prev { None => true, @@ -564,7 +577,7 @@ impl HConsign { /// Attempts to retrieve an *upgradable* value from the map. #[inline] - fn get(&self, key: &T) -> Option> { + fn get(&self, key: &T) -> Option> { if let Some(old) = self.table.get(key) { old.to_hconsed() } else { @@ -573,7 +586,7 @@ impl HConsign { } } -impl fmt::Display for HConsign +impl> fmt::Display for HConsign where T: Hash + fmt::Display, { @@ -590,7 +603,7 @@ where /// /// Implemented *via* a trait to be able to extend `RwLock` for lazy static /// consigns. -pub trait HashConsign: Sized { +pub trait HashConsign>: Sized { /// Hashconses something and returns the hash consed version. /// /// Returns `true` iff the element @@ -598,10 +611,10 @@ pub trait HashConsign: Sized { /// - was not in the consign at all, or /// - was in the consign but it is not referenced (weak ref cannot be /// upgraded.) - fn mk_is_new(self, elm: T) -> (HConsed, bool); + fn mk_is_new(self, elm: T) -> (HConsed, bool); /// Creates a HConsed element. - fn mk(self, elm: T) -> HConsed { + fn mk(self, elm: T) -> HConsed { self.mk_is_new(elm).0 } @@ -620,16 +633,16 @@ pub trait HashConsign: Sized { /// Reserves capacity for at least `additional` more elements. fn reserve(self, additional: usize); } -impl<'a, T: Hash + Eq + Clone, S: BuildHasher> HashConsign for &'a mut HConsign { - fn mk_is_new(self, elm: T) -> (HConsed, bool) { +impl<'a, T: Hash + Eq + Clone, S: BuildHasher, A: Allocator> HashConsign for &'a mut HConsign { + fn mk_is_new(self, elm: T) -> (HConsed, bool) { // If the element is known and upgradable return it. if let Some(hconsed) = self.get(&elm) { debug_assert!(*hconsed.elm == elm); return (hconsed, false); } // Otherwise build hconsed version. - let hconsed = HConsed { - elm: Arc::new(elm.clone()), + let hconsed: HConsed = HConsed { + elm: self.allocator.alloc(elm.clone()), uid: self.count, }; // Increment uid count. @@ -648,6 +661,7 @@ impl<'a, T: Hash + Eq + Clone, S: BuildHasher> HashConsign for &'a mut HConsi max_uid = None; self.table.retain(|_key, val| { + use alloc::Weak; if val.elm.strong_count() > 0 { let max = max_uid.get_or_insert(val.uid); if *max < val.uid { @@ -693,10 +707,10 @@ macro_rules! get { }; } -impl<'a, T: Hash + Eq + Clone> HashConsign for &'a RwLock> { +impl<'a, T: Hash + Eq + Clone, A: Allocator> HashConsign for &'a RwLock> { /// If the element is already in the consign, only read access will be /// requested. - fn mk_is_new(self, elm: T) -> (HConsed, bool) { + fn mk_is_new(self, elm: T) -> (HConsed, bool) { // Request read and check if element already exists. { let slf = get!(read on self); @@ -716,8 +730,8 @@ impl<'a, T: Hash + Eq + Clone> HashConsign for &'a RwLock> { } // Otherwise build hconsed version. - let hconsed = HConsed { - elm: Arc::new(elm.clone()), + let hconsed: HConsed = HConsed { + elm: slf.allocator.alloc(elm.clone()), uid: slf.count, }; // Increment uid count.