mm/vmem: add quantum caching

This commit is contained in:
Loïc Grégoire
2026-09-13 15:47:36 -04:00
parent 0c3e885797
commit 084d28b829
2 changed files with 150 additions and 73 deletions
+7 -6
View File
@@ -6,20 +6,21 @@ const mmp = mm.private;
pub fn init() void { pub fn init() void {
mmp.kernel_space.arena.init( mmp.kernel_space.arena.init(
"kernel heap", "kernel heap",
mmp.impl.kernel_heap_base, .{
r.tib(16), .base = mmp.impl.kernel_heap_base,
mm.page_size, .size = r.tib(16),
.quantum = mm.page_size,
.qcache_max = 8 * mm.page_size,
},
) catch @panic("failed to initialize kernel heap arena"); ) catch @panic("failed to initialize kernel heap arena");
} }
pub fn alloc(size: usize, policy: mm.WaitPolicy) mm.Error!*anyopaque { pub fn alloc(size: usize, policy: mm.WaitPolicy) mm.Error!*anyopaque {
mmp.kernel_space.lock.acquire();
const addr = mmp.kernel_space.arena.alloc(size, .{}) catch { const addr = mmp.kernel_space.arena.alloc(size, .{}) catch {
mmp.kernel_space.lock.release();
return mm.Error.OutOfMemory; return mm.Error.OutOfMemory;
}; };
mmp.kernel_space.lock.acquire();
const npages = size / mm.page_size; const npages = size / mm.page_size;
for (0..npages) |i| { for (0..npages) |i| {
+143 -67
View File
@@ -1,64 +1,12 @@
//! Generic resource allocator, based on the vmem allocator described in: //! Generic resource allocator, based on the vmem allocator described in:
//! Jeff Bonwick and Jonathan Adams' //! Jeff Bonwick and Jonathan Adams'
//! "Magazines and Vmem: Extending the Slab Allocator to Many CPUs and Arbitrary Resources" //! "Magazines and Vmem: Extending the Slab Allocator to Many CPUs and Arbitrary Resources"
//!
//! ## Overview
//!
//! A vmem arena manages a contiguous range of an arbitrary resource (virtual address
//! space, PIDs, etc.) by tracking free and allocated regions as a list of segments.
//! Each segment has a base address and a size.
//!
//! ## Data structures
//!
//! An arena is composed of three data structures:
//!
//! 1. Segment list: an address-ordered doubly-linked list of all segments, both
//! free and allocated. Used for coalescing on free (adjacent free segments are
//! merged) and for NextFit traversal.
//!
//! 2. Power-of-two free lists: an array of 64 lists, where bucket n holds free
//! segments whose size falls in [2^n, 2^(n+1)).
//!
//! 3. Allocated segment RB-tree: a tree of allocated segments keyed by base address.
//! Used for O(log n) lookup on free() given only an address.
//!
//! ## Allocation
//!
//! Three policies are supported:
//!
//! - `InstantFit`: find the first segment in the appropriate freelist bucket. O(1) for
//! unconstrained allocations; degrades under alignment or address constraints.
//!
//! - `BestFit`: scan freelist buckets for the smallest segment that satisfies the
//! request. Minimizes fragmentation at the cost of a fuller scan.
//!
//! - `NextFit`: continue searching from where the last allocation left off, wrapping
//! around at the end of the arena. Avoids reusing recently-freed addresses, making
//! it useful for resources like PIDs where cycling through values is desirable.
//!
//! ## Differences from the original paper
//!
//! This implementation omits several features from the original Solaris vmem:
//! - No importing: arenas are initialized with a fixed set of spans and cannot
//! grow by importing from a parent arena. This simplifies the implementation
//! significantly and is sufficient for fixed virtual address space management.
//!
//! - No span markers: without importing there is no need to track which spans
//! were imported, so span segment types are omitted entirely.
//!
//! - No quantum caching: the original vmem uses per-size slab caches (qcaches)
//! to accelerate small allocations. This is unnecessary here because vmem is
//! only called by the slab allocator for large slab-sized chunks, not for
//! individual small objects. This could change in the future if we find
//! more uses for the allocator.
//!
//! - No hash table: allocated segments are tracked in an RB-tree rather than
//! the dynamic hash table used by Solaris vmem, avoiding the resize
//! complexity.
const std = @import("std"); const std = @import("std");
const rtl = @import("rtl"); const rtl = @import("rtl");
const mm = @import("root").mm; const r = @import("root");
const mm = r.mm;
const mmp = mm.private; const mmp = mm.private;
const ke = r.ke;
pub const Policy = enum { pub const Policy = enum {
/// Use the smallest free segment that can satisfy the request. /// Use the smallest free segment that can satisfy the request.
@@ -126,6 +74,7 @@ const Segment = struct {
/// One freelist for each power of two size that can fit within the host's /// One freelist for each power of two size that can fit within the host's
/// address space. /// address space.
const freelist_count = @bitSizeOf(usize); const freelist_count = @bitSizeOf(usize);
const max_qcaches = 16;
var seg_zone: mmp.zone.TypedZone(Segment) = undefined; var seg_zone: mmp.zone.TypedZone(Segment) = undefined;
@@ -146,33 +95,63 @@ pub const Arena = struct {
allocated_segments: rtl.RBTree(Segment.cmp), allocated_segments: rtl.RBTree(Segment.cmp),
/// Last segment allocated from, for NextFit policy. /// Last segment allocated from, for NextFit policy.
rotor: ?*Segment, rotor: ?*Segment,
/// Maximum size to do quantum caching on.
qcache_max: usize,
/// Quantum caches.
qcaches: [max_qcaches]*mm.zone.Zone,
lock: ke.Mutex,
const Self = @This(); const Self = @This();
const InitOptions = struct {
base: usize,
size: usize,
quantum: usize,
qcache_max: ?usize = null,
};
/// Initialize the area for use. /// Initialize the area for use.
pub fn init( pub fn init(
self: *Self, self: *Self,
name: []const u8, name: []const u8,
base: usize, opts: InitOptions,
size: usize,
quantum: usize,
) !void { ) !void {
self.name = name; self.name = name;
self.base = base; self.base = opts.base;
self.size = size; self.size = opts.size;
self.quantum = quantum; self.quantum = opts.quantum;
self.qcache_max = opts.qcache_max orelse 0;
rtl.List.init(&self.list); self.list.init();
for (&self.freelists) |*freelist| { for (&self.freelists) |*freelist| {
freelist.init(); freelist.init();
} }
if (opts.qcache_max) |qc| {
const num_qcaches = @min(qc / opts.quantum, max_qcaches);
for (0..num_qcaches) |i| {
const size = (i + 1) * opts.quantum;
self.qcaches[i] = mm.zone.gpa.create(mm.zone.Zone) catch
unreachable;
self.qcaches[i].init(name, size, .{
.import = import,
.release = release,
.arg = self,
});
}
}
self.lock = .init();
self.allocated_segments = .init(); self.allocated_segments = .init();
self.rotor = null; self.rotor = null;
// Add initial span // Add initial span
try self.add(base, size); try self.add(self.base, self.size);
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
@@ -186,6 +165,51 @@ pub const Arena = struct {
} }
} }
fn import(
arg: ?*anyopaque,
elems: []*anyopaque,
elem_size: usize,
policy: mm.WaitPolicy,
) mm.Error!usize {
const self: *Self = @ptrCast(@alignCast(arg));
_ = policy;
self.lock.acquire();
for (0..elems.len) |i| {
elems[i] = @ptrFromInt(self.alloc_impl(
elem_size,
.{},
) catch |err| {
if (i != 0) return i;
return err;
});
}
self.lock.release();
return elems.len;
}
fn release(
arg: ?*anyopaque,
elems: []*anyopaque,
elem_size: usize,
) void {
const self: *Self = @ptrCast(@alignCast(arg));
self.lock.acquire();
for (elems) |elem| {
self.free_impl(@intFromPtr(elem), elem_size) catch {
// We did our best...
};
}
self.lock.release();
}
/// Add a span of memory to the arena. /// Add a span of memory to the arena.
pub fn add(self: *Self, addr: usize, size: usize) !void { pub fn add(self: *Self, addr: usize, size: usize) !void {
const new_seg = try self.alloc_segment(); const new_seg = try self.alloc_segment();
@@ -226,6 +250,27 @@ pub const Arena = struct {
return error.InvalidSize; return error.InvalidSize;
} }
if (size <= self.qcache_max) {
const i = size / self.quantum;
const ret = try self.qcaches[i - 1].alloc(.{});
return @intFromPtr(ret);
}
self.lock.acquire();
defer self.lock.release();
return self.alloc_impl(size, options);
}
fn alloc_impl(
self: *Self,
size: usize,
options: AllocOptions,
) mm.Error!usize {
if (size % self.quantum != 0) {
return error.InvalidSize;
}
const result = switch (options.policy) { const result = switch (options.policy) {
.InstantFit => self.instant_fit(size, options), .InstantFit => self.instant_fit(size, options),
.BestFit => self.best_fit(size, options), .BestFit => self.best_fit(size, options),
@@ -304,6 +349,20 @@ pub const Arena = struct {
} }
pub fn free(self: *Self, addr: usize, size: usize) mm.Error!void { pub fn free(self: *Self, addr: usize, size: usize) mm.Error!void {
if (size <= self.qcache_max) {
const i = size / self.quantum;
self.qcaches[i - 1].free(@ptrFromInt(addr));
return;
}
self.lock.acquire();
defer self.lock.release();
return self.free_impl(addr, size);
}
fn free_impl(self: *Self, addr: usize, size: usize) mm.Error!void {
// Find the allocated segment containing addr. // Find the allocated segment containing addr.
var search_node = Segment{ var search_node = Segment{
.type = .Allocated, .type = .Allocated,
@@ -371,13 +430,16 @@ pub const Arena = struct {
size: usize, size: usize,
options: AllocOptions, options: AllocOptions,
) ?struct { usize, *Segment } { ) ?struct { usize, *Segment } {
var idx = freelist_index(size); const is_pow2 = std.math.isPowerOfTwo(size);
const orig_idx = freelist_index(size);
if (!std.math.isPowerOfTwo(size)) idx += 1; var idx = orig_idx;
if (!is_pow2) idx += 1;
// Simply grab the first segment from the appropriate freelist // Simply grab the first segment from the appropriate freelist
// that can satisfy the request. // that can satisfy the request.
// This is O(1) unless there are contraints. // This is O(1) unless there are constraints.
while (idx < freelist_count) : (idx += 1) { while (idx < freelist_count) : (idx += 1) {
var it = self.freelists[idx].iterator(); var it = self.freelists[idx].iterator();
while (it.next()) : (it.advance()) { while (it.next()) : (it.advance()) {
@@ -386,6 +448,20 @@ pub const Arena = struct {
return .{ addr, seg }; return .{ addr, seg };
} }
} }
// If we found nothing and it's not a power of two, there may still be
// something in the lower freelist, so check there.
if (!is_pow2) {
std.debug.assert(orig_idx != 0);
var it = self.freelists[orig_idx].iterator();
while (it.next()) : (it.advance()) {
const seg = Segment.from_free_link(it.get());
if (self.try_to_fit(seg, size, options)) |addr|
return .{ addr, seg };
}
}
return null; return null;
} }
@@ -427,7 +503,7 @@ pub const Arena = struct {
options: AllocOptions, options: AllocOptions,
) ?struct { usize, *Segment } { ) ?struct { usize, *Segment } {
// Start from rotor if we have one, otherwise from the beginning. // Start from rotor if we have one, otherwise from the beginning.
const start_entry = if (self.rotor) |r| r.link.next else self.list.first(); const start_entry = if (self.rotor) |ro| ro.link.next else self.list.first();
// Search from rotor to end of list. // Search from rotor to end of list.
var entry = start_entry; var entry = start_entry;