Skip to main content

catallaxy_runtime/
buf.rs

1//! Arrays of atomic words for containers whose readers do not lock (lists, and dicts read
2//! through their seqlock). Every slot is an `AtomicU64`, so a reader racing a writer sees some
3//! value that was stored there, never a torn one. A writer that outgrows a buffer copies it into
4//! a new one, publishes the new pointer with `Release`, and hands the old one to [`release`],
5//! which frees it only once no other thread can be reading it.
6//!
7//! **Written ranges.** A buffer this module allocates is preceded by one more word: the range of
8//! slots written since the owner's last minor collection, which a minor collection of an old
9//! container scans instead of every slot (issue #120). See [`WordBuf::note_written`]. Buffers
10//! embedded in a container's own object ([`WordBuf::init_in_place`]) have no such word.
11
12use std::alloc::Layout;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15use crate::ownership::{Access, PROMOTE_ON_ACCESS};
16
17/// A capacity word followed by `cap` atomic slots.
18#[repr(C)]
19pub struct WordBuf {
20    cap: usize,
21    slots: [AtomicU64; 0],
22}
23
24/// A buffer with its written-range word in front, as this module allocates them.
25#[repr(C)]
26struct Prefixed {
27    written: AtomicU64,
28    buf: WordBuf,
29}
30
31/// Words in front of the capacity word of a buffer this module allocates.
32const PREFIX: usize = 1;
33
34/// Written-range words: the high half is the first slot written, the low half one past the last.
35/// (So a plain count `n` below 2^32 reads as the range `0..n`; see [`WordBuf::range_covers`].)
36/// Every slot, including any index too large for the encoding.
37const WRITTEN_ALL: u64 = u32::MAX as u64;
38/// No slot.
39const WRITTEN_NONE: u64 = (u32::MAX as u64) << 32;
40
41/// The buffer of every container with no capacity. Never written, never freed.
42static EMPTY: Prefixed = Prefixed { written: AtomicU64::new(WRITTEN_ALL), buf: WordBuf { cap: 0, slots: [] } };
43
44impl WordBuf {
45    fn layout(cap: usize) -> Layout {
46        Layout::from_size_align(8 * (cap + 1 + PREFIX), 8).expect("container too large")
47    }
48
49    /// The shared empty buffer.
50    pub fn empty() -> *mut WordBuf {
51        // Derived from the whole static, so the word in front stays in bounds.
52        unsafe { (&EMPTY as *const Prefixed as *mut u8).add(8 * PREFIX) as *mut WordBuf }
53    }
54
55    /// A new buffer of `cap` slots, each holding `fill`.
56    pub fn alloc(cap: usize, fill: u64) -> *mut WordBuf {
57        Self::alloc_from(cap, std::iter::empty(), fill)
58    }
59
60    /// A new buffer of `cap` slots holding the words of `prefix` (at most `cap` of them) and
61    /// then `fill`, written in one pass.
62    pub fn alloc_from(cap: usize, prefix: impl IntoIterator<Item = u64>, fill: u64) -> *mut WordBuf {
63        Self::try_alloc_from(cap, prefix, fill).unwrap_or_else(|layout| std::alloc::handle_alloc_error(layout))
64    }
65
66    /// As [`WordBuf::alloc`], but returns the layout the allocator refused instead of aborting.
67    pub fn try_alloc(cap: usize, fill: u64) -> Result<*mut WordBuf, Layout> {
68        Self::try_alloc_from(cap, std::iter::empty(), fill)
69    }
70
71    /// As [`WordBuf::alloc_from`], but returns the layout the allocator refused instead of
72    /// aborting.
73    pub fn try_alloc_from(
74        cap: usize,
75        prefix: impl IntoIterator<Item = u64>,
76        fill: u64,
77    ) -> Result<*mut WordBuf, Layout> {
78        if cap == 0 {
79            return Ok(Self::empty());
80        }
81        // A size past `isize::MAX` is refused like any other.
82        let layout = cap
83            .checked_add(1 + PREFIX)
84            .and_then(|n| Layout::array::<u64>(n).ok())
85            .ok_or_else(|| Layout::from_size_align(isize::MAX as usize & !7, 8).unwrap())?;
86        unsafe {
87            let base = std::alloc::alloc(layout) as *mut Prefixed;
88            if base.is_null() {
89                return Err(layout);
90            }
91            std::ptr::addr_of_mut!((*base).written).write(AtomicU64::new(WRITTEN_ALL));
92            let p = std::ptr::addr_of_mut!((*base).buf);
93            Self::init_in_place(p, cap, prefix, fill);
94            crate::note_external(cap * 8);
95            Ok(p)
96        }
97    }
98
99    /// Writes a buffer of `cap` slots holding the words of `prefix` (at most `cap` of them) and
100    /// then `fill` at `p`, which is not allocated by this module: containers use it for a
101    /// buffer embedded in their own heap object. Such a buffer must never be passed to
102    /// [`WordBuf::free`] or [`release`], and it has no written range: it must not be passed to
103    /// [`WordBuf::note_written`] or the functions that follow it.
104    ///
105    /// # Safety
106    /// `p` must be valid for writes of `8 * (cap + 1)` bytes, 8-aligned, and unpublished.
107    pub unsafe fn init_in_place(p: *mut WordBuf, cap: usize, prefix: impl IntoIterator<Item = u64>, fill: u64) {
108        std::ptr::addr_of_mut!((*p).cap).write(cap);
109        let slots = Self::slots_ptr(p);
110        let mut n = 0;
111        for w in prefix.into_iter().take(cap) {
112            slots.add(n).write(AtomicU64::new(w));
113            n += 1;
114        }
115        for i in n..cap {
116            slots.add(i).write(AtomicU64::new(fill));
117        }
118    }
119
120    /// A buffer holding `items` (with the vector's capacity, spare slots holding `fill`) that
121    /// takes over the vector's allocation instead of copying it: the allocation is grown by the
122    /// header words and the items are moved up within it, so building a large
123    /// container from a vector does not need memory for two copies.
124    pub fn from_vec(items: Vec<u64>, fill: u64) -> *mut WordBuf {
125        Self::try_from_vec(items, fill).unwrap_or_else(|layout| std::alloc::handle_alloc_error(layout))
126    }
127
128    /// As [`WordBuf::from_vec`], but if the allocator refuses, frees the vector and returns
129    /// the layout it refused instead of aborting.
130    pub fn try_from_vec(items: Vec<u64>, fill: u64) -> Result<*mut WordBuf, Layout> {
131        let mut items = std::mem::ManuallyDrop::new(items);
132        let (ptr, len, cap) = (items.as_mut_ptr(), items.len(), items.capacity());
133        if cap == 0 {
134            return Ok(Self::empty());
135        }
136        unsafe {
137            // `Vec<u64>` allocated `cap` words with the global allocator; `layout(cap)` has
138            // the same alignment and the header words more, so `realloc` may grow it in place.
139            let old = Layout::array::<u64>(cap).expect("container too large");
140            // A vector's allocation is at most `isize::MAX` bytes; the header words may not fit.
141            let Some(new) = cap.checked_add(1 + PREFIX).and_then(|n| Layout::array::<u64>(n).ok()) else {
142                std::alloc::dealloc(ptr as *mut u8, old);
143                return Err(old);
144            };
145            let base = std::alloc::realloc(ptr as *mut u8, old, new.size()) as *mut Prefixed;
146            if base.is_null() {
147                // The vector's allocation is still valid; free it before reporting.
148                std::alloc::dealloc(ptr as *mut u8, old);
149                return Err(new);
150            }
151            let words = base as *mut u64;
152            std::ptr::copy(words, words.add(1 + PREFIX), len);
153            std::ptr::addr_of_mut!((*base).written).write(AtomicU64::new(WRITTEN_ALL));
154            let p = std::ptr::addr_of_mut!((*base).buf);
155            std::ptr::addr_of_mut!((*p).cap).write(cap);
156            let slots = Self::slots_ptr(p);
157            for i in len..cap {
158                slots.add(i).write(AtomicU64::new(fill));
159            }
160            crate::note_external(cap * 8);
161            Ok(p)
162        }
163    }
164
165    /// # Safety
166    /// `p` must come from [`WordBuf::alloc`] and be unreachable by any reader.
167    pub unsafe fn free(p: *mut u8) {
168        let b = p as *mut WordBuf;
169        let cap = (*b).cap;
170        if cap > 0 {
171            std::alloc::dealloc(p.sub(8 * PREFIX), Self::layout(cap));
172        }
173    }
174
175    #[inline(always)]
176    fn slots_ptr(p: *const WordBuf) -> *mut AtomicU64 {
177        unsafe { (p as *mut u8).add(8) as *mut AtomicU64 }
178    }
179
180    /// # Safety
181    /// `p` must be a live buffer, loaded with `Acquire` (or by its only writer); the slice must
182    /// not be held across a safepoint.
183    #[inline(always)]
184    pub unsafe fn slots<'a>(p: *const WordBuf) -> &'a [AtomicU64] {
185        std::slice::from_raw_parts(Self::slots_ptr(p), (*p).cap)
186    }
187
188    /// # Safety
189    /// As for [`WordBuf::slots`].
190    #[inline(always)]
191    pub unsafe fn cap(p: *const WordBuf) -> usize {
192        (*p).cap
193    }
194
195    // ---- written ranges ----
196    //
197    // A minor collection must find every reference to a young object stored in an old container
198    // since the previous one. The write barrier remembers the container; these record which slots
199    // were written, so that the collection scans those instead of the whole buffer.
200    //
201    // The range must cover every slot that may hold a young reference of the owning thread. A new
202    // buffer covers everything, so a container is scanned whole the first time; a minor
203    // collection that scanned the range empties it ([`WordBuf::take_written`]), since everything
204    // it found is old afterwards. Only the owner writes an owned container, and minor collections
205    // only scan containers their thread owns, so the word needs no ordering. It is atomic because
206    // a shared container's writers (holding its lock) update it too.
207
208    #[inline(always)]
209    unsafe fn written_word<'a>(p: *const WordBuf) -> &'a AtomicU64 {
210        &*(p as *const AtomicU64).sub(PREFIX)
211    }
212
213    /// The word before the capacity word: the written range of a buffer this module allocated.
214    ///
215    /// # Safety
216    /// That word must be readable as an atomic word: `p` is a live buffer this module allocated,
217    /// or one written in place after an atomic word of the same object.
218    #[inline(always)]
219    pub unsafe fn range_word(p: *const WordBuf) -> u64 {
220        Self::written_word(p).load(Ordering::Relaxed)
221    }
222
223    /// Whether the range word `r` covers slots `lo..hi`.
224    #[inline(always)]
225    pub fn range_covers(r: u64, lo: usize, hi: usize) -> bool {
226        lo >= (r >> 32) as usize && hi <= (r as u32) as usize
227    }
228
229    /// Records that slots `lo..hi` (`lo < hi`) were written. A load and two compares when the
230    /// range already covers them, as it does for every container that is still young.
231    ///
232    /// # Safety
233    /// `p` must be a live buffer this module allocated (not one written in place), and the
234    /// caller must have write access to the container that holds it.
235    #[inline(always)]
236    pub unsafe fn note_written(p: *const WordBuf, lo: usize, hi: usize) {
237        if !Self::range_covers(Self::range_word(p), lo, hi) {
238            Self::widen_written(p, lo, hi);
239        }
240    }
241
242    /// The slow path of [`WordBuf::note_written`]: widens the range to cover `lo..hi`.
243    ///
244    /// # Safety
245    /// As for [`WordBuf::note_written`].
246    #[cold]
247    #[inline(never)]
248    pub unsafe fn widen_written(p: *const WordBuf, lo: usize, hi: usize) {
249        let w = Self::written_word(p);
250        let r = w.load(Ordering::Relaxed);
251        let (lo, hi) = (lo.min((r >> 32) as usize), hi.max((r as u32) as usize));
252        let new = if hi >= u32::MAX as usize { WRITTEN_ALL } else { ((lo as u64) << 32) | hi as u64 };
253        w.store(new, Ordering::Relaxed);
254    }
255
256    /// Gives `to`, a new buffer holding the contents of `from` at the same indices, the
257    /// written range of `from`.
258    ///
259    /// # Safety
260    /// As for [`WordBuf::note_written`], for both buffers.
261    #[inline]
262    pub unsafe fn copy_written(from: *const WordBuf, to: *const WordBuf) {
263        if (*to).cap > 0 {
264            Self::written_word(to).store(Self::written_word(from).load(Ordering::Relaxed), Ordering::Relaxed);
265        }
266    }
267
268    /// The slots written since the last call (within the capacity), which it then forgets.
269    ///
270    /// # Safety
271    /// As for [`WordBuf::note_written`]; called by a minor collection of the owning thread.
272    #[inline]
273    pub unsafe fn take_written(p: *const WordBuf) -> std::ops::Range<usize> {
274        let r = Self::written(p);
275        if (*p).cap > 0 {
276            Self::written_word(p).store(WRITTEN_NONE, Ordering::Relaxed);
277        }
278        r
279    }
280
281    /// The slots written since the last [`WordBuf::take_written`] (within the capacity).
282    ///
283    /// # Safety
284    /// As for [`WordBuf::note_written`].
285    #[inline]
286    pub unsafe fn written(p: *const WordBuf) -> std::ops::Range<usize> {
287        let cap = (*p).cap;
288        if cap == 0 {
289            return 0..0;
290        }
291        let r = Self::range_word(p);
292        let hi = ((r as u32) as usize).min(cap);
293        ((r >> 32) as usize).min(hi)..hi
294    }
295}
296
297/// Disposes of a buffer that was just replaced. With local access no other thread can be
298/// reading it, so it is freed now; otherwise the collector frees it at the next stop-the-world.
299///
300/// # Safety
301/// `old` must have been unpublished (replaced) by the caller, its only writer.
302pub unsafe fn release(old: *mut WordBuf, access: Access) {
303    if WordBuf::cap(old) == 0 {
304        return;
305    }
306    if access == Access::Local && !PROMOTE_ON_ACCESS.load(Ordering::Relaxed) {
307        WordBuf::free(old as *mut u8);
308    } else {
309        catallaxy_gc::retire(old as *mut u8, WordBuf::free);
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn refused_buffers_are_reported() {
319        // Sizes that do not fit an allocation at all. (A size the allocator refuses is covered
320        // by the out-of-memory tests of the `catallaxy` crate: Miri and the sanitizers stop at
321        // such an attempt instead of returning null.)
322        for cap in [usize::MAX / 8, usize::MAX] {
323            assert!(WordBuf::try_alloc(cap, 0).is_err(), "{cap}");
324        }
325        let b = WordBuf::try_alloc_from(4, [1, 2], 9).unwrap();
326        // SAFETY: just allocated, never published.
327        unsafe {
328            let words: Vec<u64> = WordBuf::slots(b).iter().map(|s| s.load(Ordering::Relaxed)).collect();
329            assert_eq!(words, [1, 2, 9, 9]);
330            WordBuf::free(b as *mut u8);
331        }
332    }
333}