Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ArcIntern: add from_str() specialization for ArcIntern<String> #60

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions benches/get_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,45 @@ struct NewType<T>(T);

fn bench_get_container(c: &mut Criterion) {
let mut group = c.benchmark_group("cached");
let vals: Vec<_> = (0..RANGE).map(|idx| format!("short-{}", idx)).collect();

// time: [17.635 ms 17.707 ms 17.782 ms]
group.bench_function(BenchmarkId::new("String", "short"), |b| {
b.iter_batched(
|| {},
|_| {
let mut ans = Vec::with_capacity(RANGE);
let mut ans = Vec::with_capacity(ITER);
for idx in 0..ITER {
let s = ArcIntern::<String>::new(vals[idx % RANGE].clone());
ans.push(s);
}
},
criterion::BatchSize::PerIteration,
);
});
group.bench_function(BenchmarkId::new("String", "short-from_ref"), |b| {
b.iter_batched(
|| {},
|_| {
let mut ans = Vec::with_capacity(ITER);
for idx in 0..ITER {
let s = ArcIntern::<String>::from_ref(&vals[idx % RANGE]);

ans.push(s);
}
},
criterion::BatchSize::PerIteration,
);
});

group.bench_function(BenchmarkId::new("String", "short-from_str"), |b| {
b.iter_batched(
|| {},
|_| {
let mut ans = Vec::with_capacity(ITER);
for idx in 0..ITER {
let s = ArcIntern::<String>::new(format!("short-{}", idx % RANGE));
let s = ArcIntern::<String>::from_str(&vals[idx % RANGE]);

ans.push(s);
}
},
Expand All @@ -30,18 +61,18 @@ fn bench_get_container(c: &mut Criterion) {
});
group.finish();

let new_vals: Vec<_> = (0..RANGE)
.map(|idx| NewType(format!("short-{}", idx)))
.collect();
let mut group = c.benchmark_group("uncached");
// time: [22.209 ms 22.294 ms 22.399 ms] => that's 26% faster!
group.bench_function(BenchmarkId::new("NewType<String>", "short"), |b| {
b.iter_batched(
|| {},
|_| {
let mut ans = Vec::with_capacity(RANGE);
let mut ans = Vec::with_capacity(ITER);
for idx in 0..ITER {
let s = ArcIntern::<NewType<String>>::new(NewType(format!(
"short-{}",
idx % RANGE
)));
let s = ArcIntern::<NewType<String>>::new(new_vals[idx % RANGE].clone());
ans.push(s);
}
},
Expand All @@ -54,7 +85,7 @@ fn bench_get_container(c: &mut Criterion) {
b.iter_batched(
|| {},
|_| {
let mut ans = Vec::with_capacity(RANGE);
let mut ans = Vec::with_capacity(ITER);
for idx in 0..ITER {
let s = ArcIntern::<usize>::new(idx % RANGE);
ans.push(s);
Expand All @@ -68,7 +99,7 @@ fn bench_get_container(c: &mut Criterion) {
b.iter_batched(
|| {},
|_| {
let mut ans = Vec::with_capacity(RANGE);
let mut ans = Vec::with_capacity(ITER);
for idx in 0..ITER {
let s = ArcIntern::<NewType<usize>>::new(NewType(idx % RANGE));
ans.push(s);
Expand Down
73 changes: 73 additions & 0 deletions src/arc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ impl<T: ?Sized> Borrow<RefCount<T>> for BoxRefCount<T> {
&self.0
}
}

impl<T: ?Sized + BorrowStr> Borrow<str> for BoxRefCount<T> {
#[inline(always)]
fn borrow(&self) -> &str {
&self.0.data.borrow()
}
}

impl<T: ?Sized> Deref for BoxRefCount<T> {
type Target = T;
#[inline(always)]
Expand Down Expand Up @@ -262,6 +270,60 @@ impl<T: Eq + Hash + Send + Sync + 'static> ArcIntern<T> {
}
}

/// internal trait that allows us to specialize the `Borrow` trait for `str`
/// avoid the need to create an owned value first.
pub trait BorrowStr: Borrow<str> {}

impl BorrowStr for String {}

/// BorrowStr specialization
impl<T: ?Sized + Eq + Hash + Send + Sync + 'static> ArcIntern<T> {
/// Intern a value from a reference with atomic reference counting.
///
/// this is a fast-path for str, as it avoids the need to create owned
/// value first.
pub fn from_str<Q: AsRef<str>>(val: Q) -> ArcIntern<T>
where
T: BorrowStr + for<'a> From<&'a str>,
{
// No reference only fast-path as
// the trait `std::borrow::Borrow<Q>` is not implemented for `Arc<T>`
Self::new_from_str(val.as_ref())
}

/// Intern a value from a reference with atomic reference counting.
///
/// If this value has not previously been
/// interned, then `new` will allocate a spot for the value on the
/// heap and generate that value using `T::from(val)`.
fn new_from_str<'a>(val: &'a str) -> ArcIntern<T>
where
T: BorrowStr + From<&'a str>,
{
let m = Self::get_container();
if let Some(b) = m.get(val) {
let b = b.key();
// First increment the count. We are holding the write mutex here.
// Has to be the write mutex to avoid a race
let oldval = b.0.count.fetch_add(1, Ordering::SeqCst);
if oldval != 0 {
// we can only use this value if the value is not about to be freed
return ArcIntern {
pointer: std::ptr::NonNull::from(b.0.borrow()),
};
} else {
// we have encountered a race condition here.
// we will just wait for the object to finish
// being freed.
b.0.count.fetch_sub(1, Ordering::SeqCst);
}
}

// start over with the new value
Self::new(val.into())
}
}

impl<T: ?Sized + Eq + Hash + Send + Sync + 'static> Clone for ArcIntern<T> {
fn clone(&self) -> Self {
// First increment the count. Using a relaxed ordering is
Expand Down Expand Up @@ -626,3 +688,14 @@ fn test_shrink_to_fit() {
ArcIntern::<Value>::shrink_to_fit();
assert_eq!(0, ArcIntern::<Value>::get_container().capacity());
}

#[test]
fn test_from_str() {
let x = ArcIntern::new("hello".to_string());
let y = ArcIntern::<String>::from_str("world");
assert_ne!(x, y);
assert_eq!(x, ArcIntern::from_ref("hello"));
assert_eq!(x, ArcIntern::from_str("hello"));
assert_eq!(y, ArcIntern::from_ref("world"));
assert_eq!(&*x, "hello");
}