Skip to main content

rt/
mem.rs

1// mem -- the global allocator and the memory intrinsics the compiler calls
2//
3// Small blocks (up to 2 KB) come from per-size-class free lists carved out of 64 KB
4// chunks; larger ones are whole page runs from the OS (os::pages_alloc), like mem_alloc
5// in ASMLIB.  Medium runs (up to 1 MB, rounded to a power of two of pages) are kept in a
6// cache when freed (at most CACHE_MAX bytes) and handed out again: worker threads that
7// allocate scratch per item then do not pay a VirtualAlloc / mmap, page faults and a free
8// each time.
9//
10// Threads: the small blocks live in SHARDS heaps with a spin lock each; a thread takes the one its
11// stack address leads to, so threads that allocate all the time (a simulation on worker threads does
12// so for every frame) rarely meet.  Any heap may free any block, as blocks of a class are all alike.
13// A thread that only frees would fill its heap while another one only allocates: so before a heap asks
14// for a new chunk it takes what the others hold of that class, as far as their locks are free.  The
15// cache of page runs has a lock of its own.
16
17use core::alloc::{GlobalAlloc, Layout};
18use core::ffi::c_void;
19use core::sync::atomic::{AtomicBool, Ordering};
20
21use crate::os;
22
23const CLASSES: usize = 8; // 16, 32, ..., 2048 bytes
24const CHUNK: usize = 64 * 1024;
25const PAGE: usize = 4096;
26const MID: usize = 9; // cached page runs: PAGE << k bytes, k = 0..8 (4 KB .. 1 MB)
27const CACHE_MAX: usize = 32 << 20; // bytes of page runs the cache keeps at most
28const SHARDS: usize = 64; // heaps of small blocks (a power of two: few threads share one by chance)
29
30struct Heap {
31    free: [*mut u8; CLASSES],
32    chunk: *mut u8,
33    chunk_left: usize,
34}
35
36/// a heap of small blocks on its own cache line
37#[repr(align(64))]
38struct Shard {
39    lock: AtomicBool,
40    heap: core::cell::UnsafeCell<Heap>,
41}
42
43struct Runs {
44    mid: [*mut u8; MID], // cached runs (header word 1 = the next run)
45    mid_bytes: usize,
46}
47
48pub struct Alloc {
49    shards: [Shard; SHARDS],
50    runs_lock: AtomicBool,
51    runs: core::cell::UnsafeCell<Runs>,
52}
53
54unsafe impl Sync for Alloc {}
55
56#[cfg_attr(not(feature = "std"), global_allocator)]
57static ALLOC: Alloc = Alloc::new();
58
59impl Alloc {
60    /// an empty allocator; there is one per program (the `#[global_allocator]`)
61    pub const fn new() -> Alloc {
62        Alloc {
63            shards: [const {
64                Shard {
65                    lock: AtomicBool::new(false),
66                    heap: core::cell::UnsafeCell::new(Heap { free: [core::ptr::null_mut(); CLASSES], chunk: core::ptr::null_mut(), chunk_left: 0 }),
67                }
68            }; SHARDS],
69            runs_lock: AtomicBool::new(false),
70            runs: core::cell::UnsafeCell::new(Runs { mid: [core::ptr::null_mut(); MID], mid_bytes: 0 }),
71        }
72    }
73}
74
75impl Default for Alloc {
76    fn default() -> Alloc {
77        Alloc::new()
78    }
79}
80
81/// the cache bucket of a page run of `total` bytes (rounded up to PAGE << k), None = too large
82fn mid_of(total: usize) -> Option<usize> {
83    let pages = total.div_ceil(PAGE);
84    let k = (usize::BITS - (pages - 1).leading_zeros()) as usize; // ceil(log2(pages))
85    if k < MID { Some(k) } else { None }
86}
87
88fn class_of(size: usize) -> Option<usize> {
89    let mut c = 0;
90    let mut cap = 16;
91    while c < CLASSES {
92        if size <= cap {
93            return Some(c);
94        }
95        c += 1;
96        cap <<= 1;
97    }
98    None
99}
100
101fn lock(l: &AtomicBool) {
102    while l.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() {
103        core::hint::spin_loop();
104    }
105}
106
107fn unlock(l: &AtomicBool) {
108    l.store(false, Ordering::Release);
109}
110
111impl Alloc {
112    /// the heap of this thread: by where its stack is, in steps of 1 MiB (a thread that crosses one
113    /// only changes heaps, and threads with smaller stacks may share one)
114    #[inline]
115    fn shard(&self) -> usize {
116        let probe = 0u8;
117        let a = core::ptr::addr_of!(probe) as usize >> 20;
118        a.wrapping_mul(0x9E37_79B9_7F4A_7C15_u64 as usize) >> (usize::BITS - SHARDS.trailing_zeros())
119    }
120
121    /// what another heap holds of a class, as a list (null: nothing, or their locks are taken); the
122    /// caller holds the lock of heap `me`, and no lock is waited for here
123    fn steal(&self, me: usize, c: usize) -> *mut u8 {
124        for (i, s) in self.shards.iter().enumerate() {
125            if i == me || s.lock.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() {
126                continue;
127            }
128            // SAFETY: the lock gives exclusive access to that heap's state.
129            let list = unsafe { core::mem::replace(&mut (*s.heap.get()).free[c], core::ptr::null_mut()) };
130            unlock(&s.lock);
131            if !list.is_null() {
132                return list;
133            }
134        }
135        core::ptr::null_mut()
136    }
137}
138
139impl Alloc {
140    /// the block and whether its bytes may be non-zero (reused memory)
141    unsafe fn alloc_inner(&self, layout: Layout) -> (*mut u8, bool) {
142        let size = layout.size().max(layout.align());
143        match class_of(size) {
144            Some(c) if layout.align() <= 16 => {
145                let me = self.shard();
146                let s = &self.shards[me];
147                lock(&s.lock);
148                // SAFETY: the lock gives exclusive access to the heap state.
149                let h = unsafe { &mut *s.heap.get() };
150                let bytes = 16usize << c;
151                let mut p = h.free[c];
152                // no block and no room for one: first what the others freed
153                if p.is_null() && h.chunk_left < bytes {
154                    p = self.steal(me, c);
155                }
156                let r = if !p.is_null() {
157                    // SAFETY: a free block stores the next pointer in its first word.
158                    h.free[c] = unsafe { *(p as *mut *mut u8) };
159                    p
160                } else {
161                    if h.chunk_left < bytes {
162                        // the rest of the old chunk goes to the free lists
163                        while h.chunk_left >= 16 {
164                            let mut k = CLASSES - 1;
165                            while 16usize << k > h.chunk_left {
166                                k -= 1;
167                            }
168                            // SAFETY: the block lies inside the old chunk.
169                            unsafe { *(h.chunk as *mut *mut u8) = h.free[k] };
170                            h.free[k] = h.chunk;
171                            // SAFETY: the block lies inside the old chunk.
172                            h.chunk = unsafe { h.chunk.add(16 << k) };
173                            h.chunk_left -= 16 << k;
174                        }
175                        // SAFETY: plain page allocation.
176                        h.chunk = unsafe { os::pages_alloc(CHUNK) };
177                        h.chunk_left = if h.chunk.is_null() { 0 } else { CHUNK };
178                    }
179                    if h.chunk_left < bytes {
180                        core::ptr::null_mut()
181                    } else {
182                        let q = h.chunk;
183                        // SAFETY: q + bytes stays inside the chunk.
184                        h.chunk = unsafe { h.chunk.add(bytes) };
185                        h.chunk_left -= bytes;
186                        q
187                    }
188                };
189                unlock(&s.lock);
190                (r, true)
191            }
192            _ if layout.align() > 64 => {
193                // over-aligned: the pointer is rounded up inside the run; the run's base and
194                // byte count sit right before it
195                let Some(total) = size
196                    .checked_add(layout.align() + 64 + PAGE - 1)
197                    .map(|t| t & !(PAGE - 1))
198                else {
199                    return (core::ptr::null_mut(), false);
200                };
201                // SAFETY: plain page allocation.
202                let base = unsafe { os::pages_alloc(total) };
203                if base.is_null() {
204                    return (base, false);
205                }
206                let user = (base as usize + 64).next_multiple_of(layout.align()) as *mut u8;
207                // SAFETY: user - 16 >= base and user + size <= base + total.
208                unsafe {
209                    *(user as *mut usize).sub(1) = base as usize;
210                    *(user as *mut usize).sub(2) = total;
211                }
212                (user, false)
213            }
214            _ => {
215                // page run with a 64-byte header holding the byte count
216                let mut total = (size + 64 + PAGE - 1) & !(PAGE - 1);
217                if let Some(k) = mid_of(total) {
218                    total = PAGE << k;
219                    lock(&self.runs_lock);
220                    // SAFETY: exclusive access under the lock; a cached run keeps its byte
221                    // count in header word 0 and the next cached run in word 1.
222                    let p = unsafe {
223                        let h = &mut *self.runs.get();
224                        let p = h.mid[k];
225                        if !p.is_null() {
226                            h.mid[k] = *(p as *mut *mut u8).add(1);
227                            h.mid_bytes -= total;
228                        }
229                        p
230                    };
231                    unlock(&self.runs_lock);
232                    if !p.is_null() {
233                        // SAFETY: the run is at least PAGE bytes; its header is word 0.
234                        return (unsafe { p.add(64) }, true);
235                    }
236                }
237                // SAFETY: plain page allocation.
238                let p = unsafe { os::pages_alloc(total) };
239                if p.is_null() {
240                    return (p, false);
241                }
242                // SAFETY: the header lies inside the run.
243                unsafe {
244                    *(p as *mut usize) = total;
245                    (p.add(64), false)
246                }
247            }
248        }
249    }
250}
251
252unsafe impl GlobalAlloc for Alloc {
253    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
254        // SAFETY: forwards to alloc_inner.
255        unsafe { self.alloc_inner(layout).0 }
256    }
257
258    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
259        let size = layout.size().max(layout.align());
260        match class_of(size) {
261            Some(c) if layout.align() <= 16 => {
262                let s = &self.shards[self.shard()];
263                lock(&s.lock);
264                // SAFETY: exclusive access under the lock; the block is at least 16 bytes.
265                unsafe {
266                    let h = &mut *s.heap.get();
267                    *(ptr as *mut *mut u8) = h.free[c];
268                    h.free[c] = ptr;
269                }
270                unlock(&s.lock);
271            }
272            _ if layout.align() > 64 => unsafe {
273                // SAFETY: ptr came from the over-aligned path of alloc.
274                let base = *(ptr as *mut usize).sub(1) as *mut u8;
275                let total = *(ptr as *mut usize).sub(2);
276                os::pages_free(base, total);
277            },
278            _ => unsafe {
279                // SAFETY: ptr came from the page path of alloc.
280                let base = ptr.sub(64);
281                let total = *(base as *mut usize);
282                if let Some(k) = mid_of(total).filter(|&k| PAGE << k == total) {
283                    lock(&self.runs_lock);
284                    let h = &mut *self.runs.get();
285                    let keep = h.mid_bytes + total <= CACHE_MAX;
286                    if keep {
287                        // SAFETY: exclusive access under the lock; header word 1 links the run.
288                        *(base as *mut *mut u8).add(1) = h.mid[k];
289                        h.mid[k] = base;
290                        h.mid_bytes += total;
291                    }
292                    unlock(&self.runs_lock);
293                    if keep {
294                        return;
295                    }
296                }
297                os::pages_free(base, total);
298            },
299        }
300    }
301
302    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
303        // SAFETY: forwards to alloc_inner; small blocks and cached page runs are reused
304        // memory, so clear them.  Fresh page runs come from the OS (VirtualAlloc / mmap: zero
305        // pages) and need no clearing.
306        let (p, reused) = unsafe { self.alloc_inner(layout) };
307        if !p.is_null() && reused {
308            unsafe { memset(p as *mut c_void, 0, layout.size()) };
309        }
310        p
311    }
312}
313
314// ---- memory intrinsics (no C runtime): rep movsb / stosb, so the compiler cannot turn
315// them back into calls to themselves
316#[cfg_attr(not(feature = "std"), unsafe(no_mangle))]
317pub unsafe extern "C" fn memcpy(dst: *mut c_void, src: *const c_void, n: usize) -> *mut c_void {
318    // SAFETY: the caller guarantees valid, non-overlapping ranges.
319    unsafe {
320        core::arch::asm!("rep movsb", inout("rcx") n => _, inout("rdi") dst => _,
321                         inout("rsi") src => _, options(nostack, preserves_flags));
322    }
323    dst
324}
325
326#[cfg_attr(not(feature = "std"), unsafe(no_mangle))]
327pub unsafe extern "C" fn memmove(dst: *mut c_void, src: *const c_void, n: usize) -> *mut c_void {
328    let (d, s) = (dst as *mut u8, src as *const u8);
329    if n == 0 || (d as usize) <= (s as usize) || (d as usize) >= (s as usize) + n {
330        // SAFETY: forward copy is correct when dst does not start inside src.
331        unsafe { memcpy(dst, src, n) };
332    } else {
333        // SAFETY: backward copy with the direction flag, cleared again afterwards.
334        unsafe {
335            core::arch::asm!("std", "rep movsb", "cld",
336                             inout("rcx") n => _, inout("rdi") d.add(n - 1) => _,
337                             inout("rsi") s.add(n - 1) => _, options(nostack));
338        }
339    }
340    dst
341}
342
343#[cfg_attr(not(feature = "std"), unsafe(no_mangle))]
344pub unsafe extern "C" fn memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void {
345    // SAFETY: the caller guarantees a valid range.
346    unsafe {
347        core::arch::asm!("rep stosb", inout("rcx") n => _, inout("rdi") dst => _,
348                         in("al") c as u8, options(nostack, preserves_flags));
349    }
350    dst
351}
352
353// 8 bytes through an asm load: the optimizer cannot turn the compare loop into a call to
354// memcmp / bcmp (itself)
355#[inline(always)]
356unsafe fn load8(p: *const u8) -> u64 {
357    let v: u64;
358    // SAFETY: the caller guarantees 8 readable bytes at p (any alignment).
359    unsafe {
360        core::arch::asm!("mov {v}, qword ptr [{p}]", v = out(reg) v, p = in(reg) p,
361                         options(pure, readonly, nostack, preserves_flags));
362    }
363    v
364}
365
366#[cfg_attr(not(feature = "std"), unsafe(no_mangle))]
367pub unsafe extern "C" fn memcmp(a: *const c_void, b: *const c_void, n: usize) -> i32 {
368    let (a, b) = (a as *const u8, b as *const u8);
369    let mut i = 0;
370    // 8 bytes at a time (little endian: the lowest differing byte is the first one)
371    while i + 8 <= n {
372        // SAFETY: i + 8 <= n, so both ranges hold these 8 bytes.
373        let (x, y) = unsafe { (load8(a.add(i)), load8(b.add(i))) };
374        if x != y {
375            let sh = (x ^ y).trailing_zeros() & !7;
376            return ((x >> sh) as u8) as i32 - ((y >> sh) as u8) as i32;
377        }
378        i += 8;
379    }
380    while i < n {
381        // SAFETY: the caller guarantees valid ranges of n bytes; volatile reads keep the
382        // optimizer from turning this loop back into a memcmp call.
383        let (x, y) = unsafe { (a.add(i).read_volatile(), b.add(i).read_volatile()) };
384        if x != y {
385            return x as i32 - y as i32;
386        }
387        i += 1;
388    }
389    0
390}
391
392#[cfg_attr(not(feature = "std"), unsafe(no_mangle))]
393pub unsafe extern "C" fn bcmp(a: *const c_void, b: *const c_void, n: usize) -> i32 {
394    // SAFETY: same contract as memcmp.
395    unsafe { memcmp(a, b, n) }
396}
397
398#[cfg_attr(not(feature = "std"), unsafe(no_mangle))]
399pub unsafe extern "C" fn strlen(s: *const core::ffi::c_char) -> usize {
400    let s = s as *const u8;
401    let mut n = 0;
402    // SAFETY: the caller passes a NUL-terminated string; volatile reads keep the loop a loop.
403    while unsafe { s.add(n).read_volatile() } != 0 {
404        n += 1;
405    }
406    n
407}