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}