Skip to main content

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}