Skip to content

Commit

Permalink
Optimize the implementation of Intern::default.
Browse files Browse the repository at this point in the history
The implementation of `Intern::default` used to return `Self::new(T::default())`,
which caused performance issues in workloads that call this method often.
First, `ArcIntern::new()` performs heap allocation even if the value already
exists in the interner.  Second, `default()` always hits the same shard in the
`dashmap` inside `internment` causing contention given a large enough number of
threads (in my experiments it gets pretty severe with 16 threads running on 16
CPU cores).

Note that we cannot rely on the DDlog compiler optimization that statically
evaluates function calls with constant arguments, as `Intern::default()` is
typically called from Rust, not DDlog, e.g., when deserializing fields with
`serde(default)` annotations.

The new implementation uses thread-local cache to make sure that we
call `Self::new(T::default())` at most once per type per thread.

Signed-off-by: Leonid Ryzhyk <[email protected]>
  • Loading branch information
ryzhyk committed Oct 20, 2021
1 parent 2d55ac9 commit a3db910
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 3 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Libraries

- `internment.dl`: optimized the implementation of `Intern::default()` to avoid
excessive heap allocations and contention.

### API changes

- `ddlog_clone()`: C and Java API to clone a `ddlog_record`.
Expand Down
48 changes: 45 additions & 3 deletions lib/internment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,37 @@ use differential_datalog::record::{self, Record};
use internment::ArcIntern;
use serde::{de::Deserializer, ser::Serializer};
use std::{
any::{Any, TypeId},
cell::RefCell,
cmp::{self, Ordering},
collections::HashMap,
fmt::{Debug, Display, Formatter, Result as FmtResult},
hash::{Hash, Hasher},
thread_local,
};

/// An atomically reference counted handle to an interned value.
/// In addition to memory deduplication, this type is optimized for fast comparison.
/// To this end, we store a 64-bit hash along with the interned value and use this hash for
/// comparison, only falling back to by-value comparison in case of a hash collision.
#[derive(Eq, PartialEq, Clone)]
#[derive(Eq, PartialEq)]
pub struct Intern<A>
where
A: Eq + Send + Sync + Hash + 'static,
{
interned: ArcIntern<(u64, A)>,
}

// Implement `Clone` by hand instead of auto-deriving to avoid
// bogus `T: Clone` trait bound.
impl<T: Eq + Send + Sync + Hash + 'static> Clone for Intern<T> {
fn clone(&self) -> Self {
Self {
interned: self.interned.clone(),
}
}
}

impl<T: Hash + Eq + Send + Sync + 'static> Hash for Intern<T> {
fn hash<H>(&self, state: &mut H)
where
Expand All @@ -52,9 +66,37 @@ impl<T: Hash + Eq + Send + Sync + 'static> Hash for Intern<T> {
}
}

impl<T: Default + Eq + Send + Sync + Hash + 'static> Default for Intern<T> {
impl<T: Any + Default + Eq + Send + Sync + Hash + 'static> Default for Intern<T> {
// The straightforward implementation (`Self::new(T::default())`) causes performance
// issues in workloads that call this method often.
// First, `ArcIntern::new()` performs heap allocation even if the value already
// exists in the interner. Second, `default()` always hits the same shard in the
// `dashmap` inside `internment` causing contention given a large enough number of
// threads (in my experiments it gets pretty severe with 16 threads running on 16
// CPU cores).
//
// Note that we cannot rely on the DDlog compiler optimization that statically
// evaluates function calls with constant arguments, as `Intern::default()` is
// typically called from Rust, not DDlog, e.g., when deserializing fields with
// `serde(default)` annotations.
//
// The following implementation uses thread-local cache to make sure that we will
// call `Self::new(T::default())` at most once per type per thread.
fn default() -> Self {
Self::new(T::default())
thread_local! {
static INTERN_DEFAULT: RefCell<HashMap<TypeId, Box<dyn Any + Send + Sync + 'static>>> = RefCell::new(HashMap::new());
}

INTERN_DEFAULT.with(|m| {
if let Some(v) = m.borrow().get(&TypeId::of::<T>()) {
return v.as_ref().downcast_ref::<Self>().unwrap().clone();
};

let val = Self::new(T::default());
m.borrow_mut()
.insert(TypeId::of::<T>(), Box::new(val.clone()));
val
})
}
}

Expand Down
1 change: 1 addition & 0 deletions test/datalog_tests/internment_test.dl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
input relation IInternedString(ix: istring)
relation StaticInternedString(ix: istring)

StaticInternedString(default()).
StaticInternedString(intern("static foo")).
StaticInternedString(i"ifoo").
StaticInternedString(i[|ibar|]).
Expand Down
4 changes: 4 additions & 0 deletions test/datalog_tests/internment_test.dump.expected
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
internment_test::OInternedString:
internment_test::OInternedString{.x = " bar", .ix = " bar"}: +1
internment_test::OInternedString{.x = " foo", .ix = " foo"}: +1
internment_test::OInternedString{.x = "bar", .ix = "bar"}: +1
internment_test::OInternedString{.x = "bar ", .ix = "bar "}: +1
internment_test::OInternedString{.x = "bar bar", .ix = "bar bar"}: +1
internment_test::OInternedString{.x = "bar foo", .ix = "bar foo"}: +1
internment_test::OInternedString{.x = "bar ibar", .ix = "bar ibar"}: +1
Expand All @@ -9,6 +12,7 @@ internment_test::OInternedString{.x = "bar ifoo25", .ix = "bar ifoo25"}: +1
internment_test::OInternedString{.x = "bar ifoo25!", .ix = "bar ifoo25!"}: +1
internment_test::OInternedString{.x = "bar static foo", .ix = "bar static foo"}: +1
internment_test::OInternedString{.x = "foo", .ix = "foo"}: +1
internment_test::OInternedString{.x = "foo ", .ix = "foo "}: +1
internment_test::OInternedString{.x = "foo bar", .ix = "foo bar"}: +1
internment_test::OInternedString{.x = "foo foo", .ix = "foo foo"}: +1
internment_test::OInternedString{.x = "foo ibar", .ix = "foo ibar"}: +1
Expand Down

0 comments on commit a3db910

Please sign in to comment.