string_cache/dynamic_set.rs
1// Copyright 2014 The Servo Project Developers. See the COPYRIGHT
2// file at the top-level directory of this distribution.
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10use parking_lot::Mutex;
11use std::borrow::Cow;
12use std::cell::UnsafeCell;
13use std::ptr::NonNull;
14use std::sync::OnceLock;
15use std::sync::atomic::AtomicIsize;
16use std::sync::atomic::Ordering::SeqCst;
17
18const NB_BUCKETS: usize = 1 << 12; // 4096
19const BUCKET_MASK: u64 = (1 << 12) - 1;
20
21pub(crate) struct Set {
22 buckets: Box<[Mutex<Option<NonNull<Entry>>>]>,
23}
24
25pub(crate) struct Entry {
26 // These fields can be accessed freely by `Atom` methods
27 pub(crate) string: Box<str>,
28 pub(crate) hash: u64,
29 pub(crate) ref_count: AtomicIsize,
30 // This field is protected by a `Mutex` in `Set`
31 next_in_bucket: UnsafeCell<Option<NonNull<Entry>>>,
32}
33
34// SAFETY: Access to the global linked list is strictly guarded by a Mutex,
35// and the reference counts are atomic. Even though `NonNull` is strictly
36// `!Send` and `!Sync`, the surrounding architecture makes it safe to share.
37unsafe impl Send for Entry {}
38unsafe impl Sync for Entry {}
39
40unsafe impl Send for Set {}
41unsafe impl Sync for Set {}
42
43pub(crate) fn dynamic_set() -> &'static Set {
44 // NOTE: Using const initialization for buckets breaks the small-stack test.
45 static DYNAMIC_SET: OnceLock<Set> = OnceLock::new();
46
47 DYNAMIC_SET.get_or_init(|| {
48 let buckets = (0..NB_BUCKETS).map(|_| Mutex::new(None)).collect();
49 Set { buckets }
50 })
51}
52
53impl Set {
54 pub(crate) fn insert(&self, string: Cow<str>, hash: u64) -> NonNull<Entry> {
55 let bucket_index = (hash & BUCKET_MASK) as usize;
56 let mut linked_list = self.buckets[bucket_index].lock();
57
58 {
59 let mut ptr: Option<NonNull<Entry>> = *linked_list;
60
61 while let Some(entry_ptr) = ptr {
62 // SAFETY: We hold the Mutex lock for this bucket, so no other thread can mutate
63 // the linked list. The `NonNull` pointer is guaranteed to point to a valid Entry.
64 let entry = unsafe { entry_ptr.as_ref() };
65 if entry.hash == hash && *entry.string == *string {
66 let old_size = entry.ref_count.fetch_add(1, SeqCst);
67 if old_size > 0 {
68 if old_size == isize::MAX {
69 std::process::abort();
70 }
71 return entry_ptr;
72 }
73 // Uh-oh. The pointer's reference count was zero, which means someone may try
74 // to free it. (Naive attempts to defend against this, for example having the
75 // destructor check to see whether the reference count is indeed zero, don't
76 // work due to ABA.) Thus we need to temporarily add a duplicate string to the
77 // list.
78 entry.ref_count.fetch_sub(1, SeqCst);
79 break;
80 }
81 // SAFETY: We hold the Mutex lock for this bucket, so no other thread can mutate
82 // the linked list.
83 ptr = unsafe { entry.next_in_bucket.get().read() };
84 }
85 }
86 let string = string.into_owned();
87 let entry = Box::new(Entry {
88 next_in_bucket: UnsafeCell::new(linked_list.take()),
89 hash,
90 ref_count: AtomicIsize::new(1),
91 string: string.into_boxed_str(),
92 });
93 let ptr = NonNull::from(Box::leak(entry));
94 *linked_list = Some(ptr);
95 ptr
96 }
97
98 pub(crate) fn remove(&self, ptr: *mut Entry) {
99 // SAFETY: The caller provides a pointer derived from a valid Atom. We hold the lock
100 // below, and `ptr` is guaranteed to be valid until we drop the `Box` later in this function.
101 let value: &Entry = unsafe { &*ptr };
102 let bucket_index = (value.hash & BUCKET_MASK) as usize;
103
104 let mut lock_guard = self.buckets[bucket_index].lock();
105 debug_assert!(value.ref_count.load(SeqCst) == 0);
106 let mut current: &mut Option<NonNull<Entry>> = &mut lock_guard;
107
108 while let Some(entry_ptr) = *current {
109 if entry_ptr.as_ptr() == ptr {
110 // SAFETY: The reference count has reached 0, and we hold the bucket lock.
111 // We have exclusive access to recreate the Box and deallocate the memory.
112 let unlinked_entry = unsafe { Box::from_raw(entry_ptr.as_ptr()) };
113 *current = unlinked_entry.next_in_bucket.into_inner();
114 // We’re done accessing data protected by the mutex
115 drop(lock_guard);
116 // The `Box` is deallocated here after releasing the lock
117 break;
118 }
119 // SAFETY: We hold the bucket lock, so the pointer remains valid and unaliased here.
120 // Still, don’t create `&mut Entry` here because `Atom` methods may have a `&Entry`.
121 let entry = unsafe { entry_ptr.as_ref() };
122 // SAFETY: The `UnsafeCell` is safe to access here because we hold the `Mutex`
123 current = unsafe { &mut *entry.next_in_bucket.get() };
124 }
125 }
126}
127
128#[cfg(feature = "malloc_size_of")]
129pub fn malloc_size_of_dynamic_set(ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
130 let mut sum = 0;
131 for bucket in &dynamic_set().buckets {
132 let guard = bucket.lock();
133 let mut next: Option<NonNull<Entry>> = *guard;
134 while let Some(ptr) = next {
135 // We would use `<Box<Entry> as malloc_size_of::MallocSizeOf>` here,
136 // but we don’t have `&Box<Entry>` only `NonNull<Entry>`.
137 // SAFETY: `ptr` is a valid heap-allocated pointer
138 sum += unsafe { ops.malloc_size_of::<Entry>(ptr.as_ptr()) };
139
140 // SAFETY: `ptr` is a valid pointer
141 let entry = unsafe { ptr.as_ref() };
142 sum += <Box<str> as malloc_size_of::MallocSizeOf>::size_of(&entry.string, ops);
143
144 // SAFETY: `UnsafeCell` is safe to access since we’re holding the `Mutex`
145 next = unsafe { *entry.next_in_bucket.get() };
146 }
147 }
148 sum
149}