1#![allow(unsafe_code)]
31
32use std::alloc::{GlobalAlloc, Layout, System};
33use std::cell::{Cell, UnsafeCell};
34use std::ptr;
35use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
36
37const SIZES: [usize; 16] = [
39 16, 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512,
40];
41const CLASSES: usize = SIZES.len();
42const MAX_SMALL: usize = 512;
43const MAX_ALIGN: usize = 16;
44const CHUNK: usize = 64 * 1024;
46const BATCH: usize = 32;
48const SHARDS: usize = 64;
50const MAX_CPUS: usize = 4096;
52
53const CLASS_OF: [u8; MAX_SMALL / 16 + 1] = {
55 let mut t = [0u8; MAX_SMALL / 16 + 1];
56 let mut i = 0;
57 while i < t.len() {
58 let size = if i == 0 { 1 } else { i * 16 };
59 let mut c = 0;
60 while SIZES[c] < size {
61 c += 1;
62 }
63 t[i] = c as u8;
64 i += 1;
65 }
66 t
67};
68
69#[inline]
71fn class(layout: Layout) -> Option<usize> {
72 (layout.size() <= MAX_SMALL && layout.align() <= MAX_ALIGN)
73 .then(|| usize::from(CLASS_OF[layout.size().div_ceil(16)]))
74}
75
76struct Node {
79 next: *mut Node,
80 next_batch: *mut Node,
81}
82
83#[repr(align(128))]
88struct Pool {
89 locked: AtomicBool,
90 batches: UnsafeCell<*mut Node>,
91}
92
93unsafe impl Sync for Pool {}
95
96impl Pool {
97 const fn new() -> Self {
98 Pool {
99 locked: AtomicBool::new(false),
100 batches: UnsafeCell::new(ptr::null_mut()),
101 }
102 }
103
104 fn with<T>(&self, f: impl FnOnce(&mut *mut Node) -> T) -> T {
105 let mut spins = 0u32;
106 while self
108 .locked
109 .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
110 .is_err()
111 {
112 while self.locked.load(Ordering::Relaxed) {
113 spins += 1;
114 if spins < 64 {
115 std::hint::spin_loop();
116 } else {
117 std::thread::yield_now();
118 }
119 }
120 }
121 let r = f(unsafe { &mut *self.batches.get() });
123 self.locked.store(false, Ordering::Release);
124 r
125 }
126
127 unsafe fn push(&self, first: *mut Node) {
129 self.with(|batches| {
130 unsafe { (*first).next_batch = *batches };
132 *batches = first;
133 });
134 }
135
136 unsafe fn pop(&self) -> *mut Node {
138 self.with(|batches| {
139 let first = *batches;
140 if !first.is_null() {
141 *batches = unsafe { (*first).next_batch };
143 }
144 first
145 })
146 }
147}
148
149static POOLS: [[Pool; CLASSES]; SHARDS] = [const { [const { Pool::new() }; CLASSES] }; SHARDS];
150
151static MULTI_NODE: AtomicBool = AtomicBool::new(false);
153static CPU_SHARD: [AtomicU8; MAX_CPUS] = [const { AtomicU8::new(0) }; MAX_CPUS];
155static BUDGET: AtomicUsize = AtomicUsize::new(CLASSES * CHUNK);
157
158fn shard() -> &'static [Pool; CLASSES] {
160 &POOLS[current_shard()]
161}
162
163fn current_shard() -> usize {
164 if !MULTI_NODE.load(Ordering::Relaxed) {
165 return 0;
166 }
167 #[cfg(all(target_os = "linux", target_env = "gnu"))]
168 {
169 let cpu = unsafe { libc::sched_getcpu() };
171 if let Ok(cpu) = usize::try_from(cpu)
172 && cpu < MAX_CPUS
173 {
174 return usize::from(CPU_SHARD[cpu].load(Ordering::Relaxed));
175 }
176 }
177 0
178}
179
180pub(crate) fn configure(cpu_node: &[(usize, usize)], l2_share: Option<usize>) {
182 let mut nodes = 0u64;
183 for &(cpu, node) in cpu_node {
184 let s = node % SHARDS;
185 nodes |= 1 << s;
186 if cpu < MAX_CPUS {
187 CPU_SHARD[cpu].store(s as u8, Ordering::Relaxed);
188 }
189 }
190 MULTI_NODE.store(nodes.count_ones() > 1, Ordering::Relaxed);
191 if let Some(b) = l2_share.filter(|&b| b > 0) {
192 BUDGET.store(b, Ordering::Relaxed);
193 }
194}
195
196#[doc(hidden)]
198pub fn small_cache_simulate_numa(budget: usize) {
199 let cpus: Vec<(usize, usize)> = (0..MAX_CPUS).map(|c| (c, c)).collect();
200 configure(&cpus, Some(budget));
201}
202
203struct Cache {
206 head: [Cell<*mut Node>; CLASSES],
207 len: [Cell<usize>; CLASSES],
208 max: [Cell<usize>; CLASSES],
209 bump: [Cell<*mut u8>; CLASSES],
210 left: [Cell<usize>; CLASSES],
211 bytes: Cell<usize>,
212}
213
214thread_local! {
215 static CACHE: Cache = const {
216 Cache {
217 head: [const { Cell::new(ptr::null_mut()) }; CLASSES],
218 len: [const { Cell::new(0) }; CLASSES],
219 max: [const { Cell::new(BATCH) }; CLASSES],
220 bump: [const { Cell::new(ptr::null_mut()) }; CLASSES],
221 left: [const { Cell::new(0) }; CLASSES],
222 bytes: Cell::new(0),
223 }
224 };
225}
226
227impl Cache {
228 #[inline]
229 unsafe fn alloc(&self, c: usize) -> *mut u8 {
230 let head = self.head[c].get();
231 if !head.is_null() {
232 self.head[c].set(unsafe { (*head).next });
234 self.len[c].set(self.len[c].get() - 1);
235 self.bytes.set(self.bytes.get() - SIZES[c]);
236 return head.cast();
237 }
238 unsafe { self.refill(c) }
239 }
240
241 #[cold]
243 unsafe fn refill(&self, c: usize) -> *mut u8 {
244 let size = SIZES[c];
245 let left = self.left[c].get();
246 if left > 0 {
247 let p = self.bump[c].get();
248 self.left[c].set(left - 1);
249 self.bump[c].set(unsafe { p.add(size) });
251 return p;
252 }
253 self.max[c].set(self.max[c].get().saturating_add(BATCH));
255 let first = unsafe { shard()[c].pop() };
256 if !first.is_null() {
257 let rest = unsafe { (*first).next };
260 let mut n = 0;
261 let mut p = rest;
262 while !p.is_null() {
263 n += 1;
264 p = unsafe { (*p).next };
265 }
266 self.head[c].set(rest);
267 self.len[c].set(n);
268 self.bytes.set(self.bytes.get() + n * size);
269 return first.cast();
270 }
271 let chunk = unsafe { System.alloc(Layout::from_size_align_unchecked(CHUNK, 64)) };
273 if chunk.is_null() {
274 return ptr::null_mut();
275 }
276 self.bump[c].set(unsafe { chunk.add(size) });
279 self.left[c].set(CHUNK / size - 1);
280 chunk
281 }
282
283 #[inline]
284 unsafe fn free(&self, c: usize, p: *mut u8) {
285 let (len, bytes) = unsafe { self.free_quiet(c, p) };
286 if len > self.max[c].get() || bytes > BUDGET.load(Ordering::Relaxed) {
287 unsafe { self.spill(c) };
288 }
289 }
290
291 #[inline]
293 unsafe fn free_quiet(&self, c: usize, p: *mut u8) -> (usize, usize) {
294 let node = p.cast::<Node>();
295 unsafe { (&raw mut (*node).next).write(self.head[c].get()) };
297 self.head[c].set(node);
298 let len = self.len[c].get() + 1;
299 self.len[c].set(len);
300 let bytes = self.bytes.get() + SIZES[c];
301 self.bytes.set(bytes);
302 (len, bytes)
303 }
304
305 #[cold]
308 unsafe fn spill(&self, c: usize) {
309 let pools = shard();
310 if self.len[c].get() > self.max[c].get() {
311 unsafe { self.release(pools, c, BATCH) };
312 }
313 if self.bytes.get() > BUDGET.load(Ordering::Relaxed) {
314 for c in 0..CLASSES {
315 let n = self.len[c].get().div_ceil(2);
316 unsafe { self.release(pools, c, n) };
317 }
318 }
319 }
320
321 unsafe fn release(&self, pools: &[Pool; CLASSES], c: usize, mut n: usize) {
323 while n > 0 {
324 let mut head = self.head[c].get();
325 let (first, k) = unsafe { take(&mut head, n.min(BATCH)) };
326 if k == 0 {
327 return;
328 }
329 self.head[c].set(head);
330 self.len[c].set(self.len[c].get() - k);
331 self.bytes.set(self.bytes.get() - k * SIZES[c]);
332 unsafe { pools[c].push(first) };
333 n -= k;
334 }
335 }
336}
337
338impl Drop for Cache {
339 fn drop(&mut self) {
341 let pools = shard();
342 for (c, &size) in SIZES.iter().enumerate() {
343 for _ in 0..self.left[c].replace(0) {
345 let p = self.bump[c].get();
346 self.bump[c].set(unsafe { p.add(size) });
348 unsafe { self.free_quiet(c, p) };
349 }
350 let n = self.len[c].get();
351 unsafe { self.release(pools, c, n) };
352 }
353 }
354}
355
356unsafe fn take(head: &mut *mut Node, n: usize) -> (*mut Node, usize) {
359 let first = *head;
360 if first.is_null() {
361 return (first, 0);
362 }
363 let mut last = first;
364 let mut count = 1;
365 while count < n && !unsafe { (*last).next }.is_null() {
367 last = unsafe { (*last).next };
368 count += 1;
369 }
370 *head = unsafe { (*last).next };
371 unsafe { (*last).next = ptr::null_mut() };
372 (first, count)
373}
374
375unsafe fn global_alloc(c: usize) -> *mut u8 {
377 let pools = shard();
378 let first = unsafe { pools[c].pop() };
379 if !first.is_null() {
380 let rest = unsafe { (*first).next };
382 if !rest.is_null() {
383 unsafe { pools[c].push(rest) };
384 }
385 return first.cast();
386 }
387 unsafe { System.alloc(Layout::from_size_align_unchecked(SIZES[c], MAX_ALIGN)) }
389}
390
391pub struct SmallCache;
394
395unsafe impl GlobalAlloc for SmallCache {
398 #[inline]
399 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
400 match class(layout) {
401 Some(c) => CACHE
402 .try_with(|k| unsafe { k.alloc(c) })
403 .unwrap_or_else(|_| unsafe { global_alloc(c) }),
404 None => unsafe { System.alloc(layout) },
405 }
406 }
407
408 #[inline]
409 unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
410 match class(layout) {
411 Some(c) => {
412 if CACHE.try_with(|k| unsafe { k.free(c, p) }).is_err() {
413 let node = p.cast::<Node>();
414 unsafe {
416 (&raw mut (*node).next).write(ptr::null_mut());
417 shard()[c].push(node);
418 }
419 }
420 }
421 None => unsafe { System.dealloc(p, layout) },
422 }
423 }
424
425 #[inline]
426 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
427 if class(layout).is_none() {
428 return unsafe { System.alloc_zeroed(layout) };
429 }
430 let p = unsafe { self.alloc(layout) };
431 if !p.is_null() {
432 unsafe { ptr::write_bytes(p, 0, layout.size()) };
433 }
434 p
435 }
436
437 #[inline]
438 unsafe fn realloc(&self, p: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
439 let new = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) };
441 match (class(layout), class(new)) {
442 (Some(a), Some(b)) if a == b => p,
444 (None, None) => unsafe { System.realloc(p, layout, new_size) },
446 _ => {
447 let q = unsafe { self.alloc(new) };
448 if !q.is_null() {
449 unsafe {
450 ptr::copy_nonoverlapping(p, q, layout.size().min(new_size));
451 self.dealloc(p, layout);
452 }
453 }
454 q
455 }
456 }
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 #[test]
465 fn every_small_size_maps_to_the_smallest_class_that_fits() {
466 for size in 1..=MAX_SMALL {
467 let c = class(Layout::from_size_align(size, 8).unwrap()).unwrap();
468 assert!(SIZES[c] >= size, "{size}");
469 assert!(c == 0 || SIZES[c - 1] < size, "{size}");
470 }
471 assert_eq!(class(Layout::from_size_align(0, 1).unwrap()), Some(0));
472 assert_eq!(class(Layout::from_size_align(513, 8).unwrap()), None);
473 assert_eq!(class(Layout::from_size_align(64, 32).unwrap()), None);
474 }
475
476 #[test]
477 fn a_node_fits_in_the_smallest_block_and_pools_do_not_share_lines() {
478 assert!(std::mem::size_of::<Node>() <= SIZES[0]);
479 assert_eq!(std::mem::align_of::<Pool>(), 128);
480 }
481}