lite_alloc/single_threaded/
segregated_bump.rs

1use crate::{PAGE_SIZE, grow_memory};
2
3/// Safety Warning:
4/// Allocators in this module are designed for [Single Threaded] environments.
5/// `Sync` is implemented only to satisfy `GlobalAlloc` trait requirements.
6/// Using this allocator in a multi-threaded environment will lead to Undefined Behavior (UB).
7/// Please ensure it is used only in single-threaded environments (e.g., WASM or single-threaded embedded).
8///
9/// 安全性警示 (Safety Warning):
10/// 本模块中的分配器均为【单线程】设计。
11/// 实现了 `Sync` 仅为了满足 `GlobalAlloc` trait 的要求。
12/// 在多线程环境中使用此分配器会导致未定义行为 (UB)。
13/// 请确保只在单线程环境(如 WASM 或单线程嵌入式环境)中使用。
14unsafe impl Sync for SegregatedBumpAllocator {}
15use core::{
16    alloc::{GlobalAlloc, Layout},
17    ptr::null_mut,
18};
19
20/// Minimal Allocator with Fixed Bins + Bump Pointer Fallback.
21///
22/// 极简固定分箱 + Bump Pointer 回退分配器。
23///
24/// # Design Philosophy
25/// - **Extreme Size**: Abandon Coalescing and Splitting, remove all complex list traversals and metadata headers.
26/// - **Speed First**: Small object allocation/deallocation are strictly O(1).
27/// - **Use Cases**: Wasm Serverless functions, short-lived scripts, or scenarios with ample memory but sensitive to startup speed/size.
28///
29/// # 设计哲学
30/// - **极简体积**:放弃合并(Coalescing)与拆分(Splitting),移除所有复杂的链表遍历和元数据头。
31/// - **速度优先**:小对象分配/释放均为严格的 O(1)。
32/// - **场景定位**:Wasm Serverless 函数、短生命周期脚本、或内存充裕但对启动速度/体积敏感的场景。
33///
34/// # Memory Layout
35/// - **Bin 0**: 16 Bytes (for Box<u8>, small structs)
36/// - **Bin 1**: 32 Bytes
37/// - **Bin 2**: 64 Bytes
38/// - **Bin 3**: 128 Bytes
39/// - **Large**: > 128 Bytes, allocated directly using Bump Pointer, not reused.
40///
41/// # 内存布局
42/// - **Bin 0**: 16 Bytes (用于 Box<u8>, small structs)
43/// - **Bin 1**: 32 Bytes
44/// - **Bin 2**: 64 Bytes
45/// - **Bin 3**: 128 Bytes
46/// - **Large**: > 128 Bytes,直接使用 Bump Pointer 分配,不复用。
47pub struct SegregatedBumpAllocator;
48
49impl SegregatedBumpAllocator {
50    pub const fn new() -> Self {
51        SegregatedBumpAllocator
52    }
53
54    /// ⚠️ Test/Bench only: Reset global state
55    ///
56    /// ⚠️ 仅用于测试/Bench:重置全局状态
57    pub unsafe fn reset() {
58        unsafe {
59            BINS = [null_mut(); 4];
60            HEAP_TOP = 0;
61            HEAP_END = 0;
62        }
63    }
64}
65
66// Singly linked list node, embedded in free memory blocks
67// 单链表节点,嵌入在空闲内存块中
68struct Node {
69    next: *mut Node,
70}
71
72// --------------------------------------------------------------------------
73// Global Static State (Safe in single-threaded Wasm)
74// 全局静态状态 (在单线程 Wasm 中是安全的)
75// --------------------------------------------------------------------------
76
77// 4 bins head pointers. BINS[0] -> 16B, [1] -> 32B, [2] -> 64B, [3] -> 128B
78// 4个桶的头指针。BINS[0] -> 16B, [1] -> 32B, [2] -> 64B, [3] -> 128B
79static mut BINS: [*mut Node; 4] = [null_mut(); 4];
80
81// Bump Pointer (Heap Top Pointer)
82// Bump Pointer (堆顶指针)
83static mut HEAP_TOP: usize = 0;
84// Current Wasm memory boundary
85// 当前已申请的 Wasm 内存边界
86static mut HEAP_END: usize = 0;
87
88unsafe impl GlobalAlloc for SegregatedBumpAllocator {
89    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
90        // 1. Large alignment handling
91        // Fixed Bins default guarantee 16-byte alignment.
92        // If user requests > 16-byte alignment (very rare), handle directly via Bump allocation.
93        // 1. 大对齐处理
94        // 固定 Bins 默认保证 16 字节对齐。
95        // 如果用户请求 > 16 字节对齐(非常罕见),直接通过 Bump 分配来处理对齐。
96        if layout.align() > 16 {
97            return unsafe { self.bump_alloc(layout.size(), layout.align()) };
98        }
99
100        // 2. Calculate category
101        // 2. 计算分类
102        let size = layout.size().max(16);
103
104        // 3. Try lookup reuse (Small Alloc)
105        // 3. 尝试查表复用 (Small Alloc)
106        if let Some(index) = get_index(size) {
107            unsafe {
108                let head = BINS[index];
109                if !head.is_null() {
110                    // Hit: Pop from list head (LIFO)
111                    // Hit: 弹出链表头 (LIFO)
112                    let next = (*head).next;
113                    BINS[index] = next;
114                    return head as *mut u8;
115                }
116            }
117
118            // Miss: Bin is empty, fallback to Bump allocation
119            // Allocate block of corresponding Bin size directly, instead of layout.size(), so it can be returned correctly later
120            // Miss: Bin 为空,回退到 Bump 分配
121            // 直接分配对应 Bin 大小的块,而不是 layout.size(),以便将来 dealloc 能正确归位
122            let block_size = 16 << index;
123            return unsafe { self.bump_alloc(block_size, 16) };
124        }
125
126        // 4. Large object handling (> 128 Bytes)
127        // Alloc via Bump directly, skip Bins
128        // 4. 大对象处理 (> 128 Bytes)
129        // 直接 Bump 分配,不走 Bin
130        unsafe { self.bump_alloc(size, 16) }
131    }
132
133    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
134        // 1. If block has high alignment requirement, it must not be from Bins,
135        //    and because we don't have metadata for its size, just discard it (leak).
136        // 1. 如果是对齐要求很高的块,它一定不是来自 Bins,
137        //    且我们没有元数据记录它的大小,所以直接丢弃(泄漏)。
138        if layout.align() > 16 {
139            return;
140        }
141
142        let size = layout.size().max(16);
143
144        // 2. Try to return to Bins
145        // 2. 尝试归还到 Bins
146        if let Some(index) = get_index(size) {
147            let node = ptr as *mut Node;
148            // Insert at head (O(1))
149            // 头插法 (O(1))
150            unsafe {
151                (*node).next = BINS[index];
152                BINS[index] = node;
153            }
154            return;
155        }
156
157        // 3. Large Object (> 128 Bytes)
158        // Strategy choice: Abandon large object reuse for minimal size.
159        // They will be reclaimed when Wasm instance is destroyed.
160        // 3. 大对象 (> 128 Bytes)
161        // 策略选择:为了极简体积,放弃大对象复用。
162        // 它们会随 Wasm 实例销毁而回收。
163    }
164
165    #[cfg(feature = "realloc")]
166    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
167        // 1. Determine actual capacity of old block
168        // 1. 确定旧块的实际容量
169        let old_size = layout.size().max(16);
170        let old_capacity = if layout.align() > 16 {
171            old_size
172        } else if let Some(index) = get_index(old_size) {
173            16 << index
174        } else {
175            old_size
176        };
177
178        // 2. If new size <= old capacity, reuse directly (In-place shrink)
179        // 2. 如果新大小 <= 旧容量,直接复用 (In-place shrink)
180        if new_size <= old_capacity {
181            return ptr;
182        }
183
184        // 3. Try to grow in place (In-place grow at HEAP_TOP)
185        // Only possible if ptr is exactly at heap top.
186        // 3. 尝试原地扩容 (In-place grow at HEAP_TOP)
187        // 只有当 ptr 恰好在堆顶时才可能。
188        let heap_top = unsafe { HEAP_TOP };
189        if ptr as usize + old_capacity == heap_top {
190            let diff = new_size - old_capacity;
191            let heap_end = unsafe { HEAP_END };
192
193            // Check if there is enough remaining space or grow memory
194            // 检查是否有足够的剩余空间或扩容
195            unsafe {
196                if HEAP_TOP + diff <= heap_end {
197                    HEAP_TOP += diff;
198                    return ptr;
199                }
200
201                let pages_needed =
202                    ((HEAP_TOP + diff - heap_end + PAGE_SIZE - 1) / PAGE_SIZE).max(1);
203                if grow_memory(pages_needed) != usize::MAX {
204                    HEAP_END += pages_needed * PAGE_SIZE;
205                    HEAP_TOP += diff;
206                    return ptr;
207                }
208            }
209        }
210
211        // 4. Default fallback: Alloc + Copy + Dealloc
212        // 4. 默认回退:Alloc + Copy + Dealloc
213        unsafe {
214            let new_ptr = self.alloc(Layout::from_size_align_unchecked(new_size, layout.align()));
215            if !new_ptr.is_null() {
216                // copy old_size, not old_capacity, because data is only valid up to old_size
217                core::ptr::copy_nonoverlapping(ptr, new_ptr, layout.size());
218                self.dealloc(ptr, layout);
219            }
220            new_ptr
221        }
222    }
223}
224
225impl SegregatedBumpAllocator {
226    /// Core Bump Pointer allocation logic
227    /// 核心 Bump Pointer 分配逻辑
228    unsafe fn bump_alloc(&self, size: usize, align: usize) -> *mut u8 {
229        unsafe {
230            let mut ptr = HEAP_TOP;
231
232            // Handle alignment: (ptr + align - 1) & !(align - 1)
233            // For align=16, it means (ptr + 15) & !15
234            // 处理对齐: (ptr + align - 1) & !(align - 1)
235            // 对于 align=16,即 (ptr + 15) & !15
236            ptr = (ptr + align - 1) & !(align - 1);
237
238            // Check for overflow or insufficient capacity
239            // 检查溢出或容量不足
240            if ptr + size > HEAP_END || ptr < HEAP_TOP {
241                // How many pages needed?
242                // 需要多少页?
243                let bytes_needed = (ptr + size).saturating_sub(HEAP_END);
244                let pages_needed = ((bytes_needed + PAGE_SIZE - 1) / PAGE_SIZE).max(1);
245
246                let prev_page = grow_memory(pages_needed);
247                if prev_page == usize::MAX {
248                    return null_mut(); // OOM
249                }
250
251                // If initial allocation (HEAP_END == 0), need to initialize ptr
252                // 如果是初次分配 (HEAP_END == 0),需要初始化 ptr
253                if HEAP_END == 0 {
254                    // prev_page should be 0 (or existing memory size)
255                    // Wasm memory_grow returns old page count
256                    // prev_page 应该是 0 (或者现有内存大小)
257                    // Wasm memory_grow 返回旧的页数
258                    let memory_start = prev_page * PAGE_SIZE;
259                    ptr = memory_start;
260                    // Re-align
261                    // 再次对齐
262                    ptr = (ptr + align - 1) & !(align - 1);
263
264                    HEAP_END = memory_start + pages_needed * PAGE_SIZE;
265                } else {
266                    HEAP_END += pages_needed * PAGE_SIZE;
267                }
268            }
269
270            HEAP_TOP = ptr + size;
271            ptr as *mut u8
272        }
273    }
274}
275
276// --------------------------------------------------------------------------
277// Helper Functions
278// 辅助函数
279// --------------------------------------------------------------------------
280
281/// Get Bin index based on size.
282/// 0 -> 16B, 1 -> 32B, 2 -> 64B, 3 -> 128B
283/// Returns None indicating large object.
284///
285/// 根据大小获取 Bin 索引。
286/// 0 -> 16B, 1 -> 32B, 2 -> 64B, 3 -> 128B
287/// 返回 None 表示是大对象。
288#[inline(always)]
289fn get_index(size: usize) -> Option<usize> {
290    if size > 128 {
291        return None;
292    }
293
294    // Use CLZ (Count Leading Zeros) instruction to quickly calculate log2
295    // size 16 (10000) -> index 0
296    // size 17..32 -> index 1
297    // ...
298    // 利用 CLZ (Count Leading Zeros) 指令快速计算 log2
299    // size 16 (10000) -> index 0
300    // size 17..32 -> index 1
301    // ...
302    let size_val = size as usize;
303
304    // next_power_of_two ensures 17 becomes 32
305    // next_power_of_two 确保 17 变成 32
306    let power_of_two = size_val.next_power_of_two();
307    let zeros = power_of_two.leading_zeros();
308
309    // Calculate base offset.
310    // u32: leading_zeros(16) = 27.  Target index = 0. => 27 - 27 = 0
311    // u64: leading_zeros(16) = 59.  Target index = 0. => 59 - 59 = 0
312    // 计算基准偏移。
313    // u32: leading_zeros(16) = 27.  Target index = 0. => 27 - 27 = 0
314    // u64: leading_zeros(16) = 59.  Target index = 0. => 59 - 59 = 0
315    #[cfg(target_pointer_width = "32")]
316    const BASE: u32 = 27;
317
318    #[cfg(target_pointer_width = "64")]
319    const BASE: u32 = 59;
320
321    Some((BASE - zeros) as usize)
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use std::sync::Mutex;
328
329    // Use a global lock to serialize tests because SegregatedBumpAllocator uses global `static mut` state.
330    static TEST_LOCK: Mutex<()> = Mutex::new(());
331
332    fn with_clean_allocator(f: impl FnOnce()) {
333        let _guard = TEST_LOCK.lock().unwrap();
334        unsafe {
335            // 1. Reset internal global state
336            SegregatedBumpAllocator::reset();
337
338            // 2. Reset host memory simulation (lib.rs)
339            crate::reset_heap();
340        }
341        f();
342    }
343
344    #[test]
345    fn test_small_alloc_reuse() {
346        with_clean_allocator(|| {
347            let allocator = SegregatedBumpAllocator::new();
348            let layout = Layout::from_size_align(16, 8).unwrap(); // Bin 0: 16B
349
350            unsafe {
351                // 1. Alloc (Miss -> Bump)
352                let ptr1 = allocator.alloc(layout);
353                assert!(!ptr1.is_null());
354                *ptr1 = 0xAA;
355
356                // 2. Dealloc (Push to Bin 0)
357                allocator.dealloc(ptr1, layout);
358
359                // 3. Alloc (Hit -> Reuse)
360                let ptr2 = allocator.alloc(layout);
361                assert_eq!(ptr1, ptr2, "Should reuse the same pointer from bin");
362                // Intrusive list overwrites the first bytes with 'next' pointer, so data is not preserved.
363            }
364        });
365    }
366
367    #[test]
368    fn test_cross_bin_isolation() {
369        with_clean_allocator(|| {
370            let allocator = SegregatedBumpAllocator::new();
371
372            unsafe {
373                // Bin 0 (16B)
374                let l0 = Layout::from_size_align(10, 1).unwrap();
375                let p0 = allocator.alloc(l0);
376
377                // Bin 1 (32B)
378                let l1 = Layout::from_size_align(20, 1).unwrap();
379                let p1 = allocator.alloc(l1);
380
381                assert_ne!(p0, p1);
382
383                // Dealloc 16B block
384                allocator.dealloc(p0, l0);
385
386                // Alloc 32B block - should NOT reuse 16B block
387                let p2 = allocator.alloc(l1);
388                assert_ne!(p0, p2);
389
390                // Alloc 16B block - should reuse p0
391                let p3 = allocator.alloc(l0);
392                assert_eq!(p0, p3);
393            }
394        });
395    }
396
397    #[test]
398    fn test_large_alloc_bypass() {
399        with_clean_allocator(|| {
400            let allocator = SegregatedBumpAllocator::new();
401            // > 128 Bytes -> Large alloc (Bump directly)
402            let layout = Layout::from_size_align(200, 16).unwrap();
403
404            unsafe {
405                let p1 = allocator.alloc(layout);
406                assert!(!p1.is_null());
407
408                // Dealloc (No-op usually for large allocs in this simple allocator)
409                allocator.dealloc(p1, layout);
410
411                // Alloc again. Since dealloc is no-op, it allocates new space.
412                let p2 = allocator.alloc(layout);
413                assert_ne!(
414                    p1, p2,
415                    "Large objects should not be implicitly reused in this implementation"
416                );
417            }
418        });
419    }
420
421    #[test]
422    fn test_memory_growth() {
423        with_clean_allocator(|| {
424            let allocator = SegregatedBumpAllocator::new();
425            // Alloc enough to force page growth. PAGE_SIZE is 64KB.
426            // Let's alloc 40KB twice.
427            let layout = Layout::from_size_align(40 * 1024, 16).unwrap();
428
429            unsafe {
430                let p1 = allocator.alloc(layout);
431                assert!(!p1.is_null());
432
433                let p2 = allocator.alloc(layout);
434                assert!(!p2.is_null());
435                assert_ne!(p1, p2);
436
437                // Verify they don't overlap
438                let dist = (p2 as usize).wrapping_sub(p1 as usize);
439                assert!(dist >= 40 * 1024);
440            }
441        });
442    }
443
444    #[test]
445    fn test_high_alignment() {
446        with_clean_allocator(|| {
447            let allocator = SegregatedBumpAllocator::new();
448            // 32B size, but 128B alignment.
449            // Should bypass bins (max align 16) and use bump with align pad.
450            let layout = Layout::from_size_align(32, 128).unwrap();
451
452            unsafe {
453                let p1 = allocator.alloc(layout);
454                assert!(!p1.is_null());
455                assert_eq!(p1 as usize % 128, 0);
456
457                allocator.dealloc(p1, layout); // Should be no-op or handled gracefully
458            }
459        });
460    }
461
462    #[test]
463    fn test_realloc_shrink() {
464        with_clean_allocator(|| {
465            let allocator = SegregatedBumpAllocator::new();
466            // Bin 3: 128B
467            let layout = Layout::from_size_align(100, 16).unwrap();
468            // capacity will be 128
469
470            unsafe {
471                let ptr = allocator.alloc(layout);
472                ptr.write_bytes(0xCC, 100);
473
474                // Shrink to 80. Still Bin 3 (128B). Should be inplace.
475                let new_size = 80;
476                let new_ptr = allocator.realloc(ptr, layout, new_size);
477
478                #[cfg(feature = "realloc")]
479                assert_eq!(ptr, new_ptr);
480                #[cfg(not(feature = "realloc"))]
481                assert_ne!(ptr, new_ptr); // Default moves
482
483                assert_eq!(*new_ptr, 0xCC);
484            }
485        });
486    }
487
488    #[test]
489    fn test_realloc_grow_large_at_top() {
490        with_clean_allocator(|| {
491            let allocator = SegregatedBumpAllocator::new();
492            // Large alloc > 128B.
493            let layout = Layout::from_size_align(256, 16).unwrap();
494
495            unsafe {
496                let ptr = allocator.alloc(layout);
497                ptr.write_bytes(0xAA, 256);
498
499                // This should be at HEAP_TOP.
500
501                let new_size = 512;
502                let new_ptr = allocator.realloc(ptr, layout, new_size);
503
504                #[cfg(feature = "realloc")]
505                assert_eq!(ptr, new_ptr); // Should grow in place
506                #[cfg(not(feature = "realloc"))]
507                assert_ne!(ptr, new_ptr);
508
509                assert_eq!(*new_ptr, 0xAA);
510                assert_eq!(*new_ptr.add(255), 0xAA);
511            }
512        });
513    }
514
515    #[test]
516    fn test_realloc_move() {
517        with_clean_allocator(|| {
518            let allocator = SegregatedBumpAllocator::new();
519            // 1. Alloc something to block bottom
520            let l1 = Layout::from_size_align(64, 16).unwrap();
521            let _ = unsafe { allocator.alloc(l1) };
522
523            // 2. Alloc target block (Large)
524            let layout = Layout::from_size_align(256, 16).unwrap();
525            let ptr = unsafe { allocator.alloc(layout) };
526
527            // 3. Alloc something else to block top (so ptr is NOT at HEAP_TOP)
528            let l3 = Layout::from_size_align(64, 16).unwrap();
529            let _p3 = unsafe { allocator.alloc(l3) };
530
531            unsafe {
532                ptr.write_bytes(0xBB, 256);
533
534                let new_size = 512;
535                let new_ptr = allocator.realloc(ptr, layout, new_size);
536
537                assert_ne!(ptr, new_ptr); // Must move
538                assert_eq!(*new_ptr, 0xBB);
539            }
540        });
541    }
542}