Add fixed stack-backed allocator implementation
This commit is contained in:
		
							parent
							
								
									fcd4ecd85d
								
							
						
					
					
						commit
						14b3921001
					
				@ -4,11 +4,12 @@ const stack = @import("./stack.zig");
 | 
			
		||||
const testing = @import("./testing.zig");
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
///
 | 
			
		||||
/// Allocation options for an [Allocator].
 | 
			
		||||
///
 | 
			
		||||
pub const Allocation = struct {
 | 
			
		||||
    existing: ?[*]u8,
 | 
			
		||||
    size: usize
 | 
			
		||||
    alignment: u29,
 | 
			
		||||
    size: usize,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
@ -17,29 +18,7 @@ pub const Allocation = struct {
 | 
			
		||||
pub const Allocator = meta.Function(Allocation, ?[*]u8);
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
///
 | 
			
		||||
///
 | 
			
		||||
pub const ArenaAllocator = struct {
 | 
			
		||||
    region: []u8,
 | 
			
		||||
    cursor: usize = 0,
 | 
			
		||||
 | 
			
		||||
    ///
 | 
			
		||||
    ///
 | 
			
		||||
    ///
 | 
			
		||||
    pub fn allocator(arena_allocator: *ArenaAllocator) Allocator {
 | 
			
		||||
        return Allocator.fromClosure(arena_allocator, struct {
 | 
			
		||||
            fn call(context: *ArenaAllocator, allocation: Allocation) ?[*]u8 {
 | 
			
		||||
                _ = allocation;
 | 
			
		||||
                _ = context;
 | 
			
		||||
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        }.call);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
///
 | 
			
		||||
/// [MakeError.OutOfMemory] if the requested amount of memory could not be allocated.
 | 
			
		||||
///
 | 
			
		||||
pub const MakeError = error {
 | 
			
		||||
    OutOfMemory,
 | 
			
		||||
@ -259,6 +238,21 @@ test "Check memory is equal" {
 | 
			
		||||
    try testing.expect(!equals(u8, bytes_sequence, &.{69, 42}));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
/// Fills the contents of `target` with `source`.
 | 
			
		||||
///
 | 
			
		||||
pub fn fill(comptime Element: type, target: []Element, source: Element) void {
 | 
			
		||||
    for (target) |_, index| target[index] = source;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test "Fill data" {
 | 
			
		||||
    var buffer = [_]u32{0} ** 8;
 | 
			
		||||
 | 
			
		||||
    fill(u32, &buffer, 1);
 | 
			
		||||
 | 
			
		||||
    for (buffer) |element| try testing.expect(element == 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
/// Searches for the first instance of an `Element` equal to `needle` in `haystack`, returning its
 | 
			
		||||
/// index or `null` if nothing was found.
 | 
			
		||||
@ -318,7 +312,10 @@ test "Find first of sequence" {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
/// Frees `allocated_memory` using `allocator`.
 | 
			
		||||
///
 | 
			
		||||
/// *Note* that only memory known to be freeable by `allocator` should be passed via
 | 
			
		||||
/// `allocated_memory`. Anything else will result is considered unreachable logic.
 | 
			
		||||
///
 | 
			
		||||
pub fn free(allocator: Allocator, allocated_memory: anytype) void {
 | 
			
		||||
    if (allocator.call(.{
 | 
			
		||||
@ -332,6 +329,7 @@ pub fn free(allocator: Allocator, allocated_memory: anytype) void {
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
        .size = 0,
 | 
			
		||||
        .alignment = 0,
 | 
			
		||||
    }) != null) unreachable;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -356,13 +354,21 @@ test "Hashing bytes" {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
///
 | 
			
		||||
/// Attempts to allocate a buffer of `size` `Element`s using `allocator`, returning it or a
 | 
			
		||||
/// [MakeError] if it failed.
 | 
			
		||||
///
 | 
			
		||||
pub fn makeMany(comptime Element: type, allocator: Allocator, size: usize) MakeError![*]Element {
 | 
			
		||||
    return @ptrCast([*]Element, @alignCast(@alignOf(Element), allocator.call(.{
 | 
			
		||||
    const alignment = @alignOf(Element);
 | 
			
		||||
 | 
			
		||||
    if (allocator.call(.{
 | 
			
		||||
        .existing = null,
 | 
			
		||||
        .size = size,
 | 
			
		||||
    }) orelse return error.OutOfMemory));
 | 
			
		||||
        .size = @sizeOf(Element) * size,
 | 
			
		||||
        .alignment = alignment,
 | 
			
		||||
    })) |buffer| {
 | 
			
		||||
        return @ptrCast([*]Element, @alignCast(alignment, buffer));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return error.OutOfMemory;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
@ -385,7 +391,8 @@ test "Data swapping" {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
/// [Writer] that silently consumes all given data without failure and throws it away.
 | 
			
		||||
/// Thread-safe and lock-free [Writer] that silently consumes all given data without failure and
 | 
			
		||||
/// throws it away.
 | 
			
		||||
///
 | 
			
		||||
/// This is commonly used for testing or redirected otherwise unwanted output data that has to be
 | 
			
		||||
/// sent somewhere for whatever reason.
 | 
			
		||||
 | 
			
		||||
@ -69,18 +69,16 @@ pub fn Function(comptime In: type, comptime Out: type) type {
 | 
			
		||||
                else => @compileError("`context` must be a pointer"),
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var function = Self{
 | 
			
		||||
            return Self{
 | 
			
		||||
                .context = @ptrCast(*anyopaque, context),
 | 
			
		||||
 | 
			
		||||
                .callErased = struct {
 | 
			
		||||
                    fn callErased(erased: *anyopaque, input: In) Out {
 | 
			
		||||
                        return if (Context == void) invoke(input) else invoke(@ptrCast(
 | 
			
		||||
                            *Context, @alignCast(@alignOf(Context), erased)).*, input);
 | 
			
		||||
                            Context, @alignCast(@alignOf(Context), erased)), input);
 | 
			
		||||
                    }
 | 
			
		||||
                }.callErased,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            return function;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ pub fn Fixed(comptime Element: type) type {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ///
 | 
			
		||||
        /// Attempts to push `element` into `self`, returning a [FixedPushError] if it failed.
 | 
			
		||||
        /// Attempts to push `element` into `self`, returning a [PushError] if it failed.
 | 
			
		||||
        ///
 | 
			
		||||
        pub fn push(self: *Self, element: Element) PushError!void {
 | 
			
		||||
            if (self.filled == self.buffer.len) return error.OutOfMemory;
 | 
			
		||||
@ -49,8 +49,7 @@ pub fn Fixed(comptime Element: type) type {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ///
 | 
			
		||||
        /// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it
 | 
			
		||||
        /// failed.
 | 
			
		||||
        /// Attempts to push all of `elements` into `self`, returning a [PushError] if it failed.
 | 
			
		||||
        ///
 | 
			
		||||
        pub fn pushAll(self: *Self, elements: []const Element) PushError!void {
 | 
			
		||||
            const filled = (self.filled + elements.len);
 | 
			
		||||
@ -61,6 +60,20 @@ pub fn Fixed(comptime Element: type) type {
 | 
			
		||||
 | 
			
		||||
            self.filled = filled;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ///
 | 
			
		||||
        /// Attempts to push `count` instances of `element` into `self`, returning a [PushError] if
 | 
			
		||||
        /// it failed.
 | 
			
		||||
        ///
 | 
			
		||||
        pub fn pushMany(self: *Self, element: Element, count: usize) PushError!void {
 | 
			
		||||
            const filled = (self.filled + count);
 | 
			
		||||
 | 
			
		||||
            if (filled > self.buffer.len) return error.OutOfMemory;
 | 
			
		||||
 | 
			
		||||
            io.fill(Element, self.buffer[self.filled ..], element);
 | 
			
		||||
 | 
			
		||||
            self.filled = filled;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -115,6 +128,45 @@ test "Fixed stack of string literals" {
 | 
			
		||||
///
 | 
			
		||||
pub const PushError = io.MakeError;
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
/// Creates and returns a [io.Allocator] value wrapping `fixed_stack`.
 | 
			
		||||
///
 | 
			
		||||
/// The returned [io.Allocator] uses `fixed_stack` and its backing memory buffer as a fixed-length
 | 
			
		||||
/// memory pool to linearly allocate memory from.
 | 
			
		||||
///
 | 
			
		||||
pub fn fixedAllocator(fixed_stack: *Fixed(u8)) io.Allocator {
 | 
			
		||||
    return io.Allocator.fromClosure(fixed_stack, struct {
 | 
			
		||||
        fn alloc(stack: *Fixed(u8), allocation: io.Allocation) ?[*]u8 {
 | 
			
		||||
            if (allocation.existing) |buffer| if (allocation.size == 0) {
 | 
			
		||||
                // Deallocate the memory.
 | 
			
		||||
                const buffer_address = @ptrToInt(buffer);
 | 
			
		||||
                const stack_buffer_address = @ptrToInt(stack.buffer.ptr);
 | 
			
		||||
 | 
			
		||||
                // Check the buffer is within the address space of the stack buffer. If not, it
 | 
			
		||||
                // should just be returned to let the caller know it cannot be freed.
 | 
			
		||||
                if ((buffer_address < stack_buffer_address) or
 | 
			
		||||
                    (buffer_address >= (stack_buffer_address + stack.filled))) return buffer;
 | 
			
		||||
 | 
			
		||||
                // TODO: Investigate ways of freeing if it is the last allocation.
 | 
			
		||||
                return null;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Reallocate / allocate the memory.
 | 
			
		||||
            // TODO: Remove stdlib dependency.
 | 
			
		||||
            const adjusted_offset = @import("std").mem.alignPointerOffset(stack.buffer.ptr +
 | 
			
		||||
                stack.filled, allocation.alignment) orelse return null;
 | 
			
		||||
 | 
			
		||||
            const head = stack.filled + adjusted_offset;
 | 
			
		||||
            const tail = head + allocation.size;
 | 
			
		||||
 | 
			
		||||
            stack.pushMany(0, tail) catch return null;
 | 
			
		||||
 | 
			
		||||
            return stack.buffer[head .. tail].ptr;
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }.alloc);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
/// Returns an [io.Writer] wrapping `fixed_stack`.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
const io = @import("./io.zig");
 | 
			
		||||
const stack = @import("./stack.zig");
 | 
			
		||||
const testing = @import("./testing.zig");
 | 
			
		||||
 | 
			
		||||
///
 | 
			
		||||
@ -48,10 +49,10 @@ pub fn Hashed(comptime Key: type, comptime Value: type,
 | 
			
		||||
        /// Returns a new [Self] value or an [io.MakeError] if initializing failed.
 | 
			
		||||
        ///
 | 
			
		||||
        pub fn init(allocator: Allocator) io.MakeError!Self {
 | 
			
		||||
            const initial_capacity = 4;
 | 
			
		||||
            const capacity = 4;
 | 
			
		||||
 | 
			
		||||
            return Self{
 | 
			
		||||
                .buckets = (try io.makeMany(Bucket, allocator, initial_capacity))[0 .. initial_capacity],
 | 
			
		||||
                .buckets = (try io.makeMany(Bucket, allocator, capacity))[0 .. capacity],
 | 
			
		||||
                .filled = 0,
 | 
			
		||||
                .allocator = allocator,
 | 
			
		||||
                .load_limit = 0.75,
 | 
			
		||||
@ -197,15 +198,15 @@ pub const string_literal_context = KeyContext([]const u8){
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
test "Hash table manipulation with string literal context" {
 | 
			
		||||
    var buffer = [_]u8{0} ** 1024;
 | 
			
		||||
    var arena_allocator = io.ArenaAllocator{.region = &buffer};
 | 
			
		||||
    var buffer = [_]u8{0} ** 4096;
 | 
			
		||||
    var fixed_stack = stack.Fixed(u8){.buffer = &buffer};
 | 
			
		||||
 | 
			
		||||
    var table =
 | 
			
		||||
        try Hashed([]const u8, u32, string_literal_context).init(arena_allocator.allocator());
 | 
			
		||||
    var table = try Hashed([]const u8, u32, string_literal_context).
 | 
			
		||||
        init(stack.fixedAllocator(&fixed_stack));
 | 
			
		||||
 | 
			
		||||
    defer table.deinit();
 | 
			
		||||
 | 
			
		||||
    const foo = @as(u32, 69);
 | 
			
		||||
    const foo = 69;
 | 
			
		||||
 | 
			
		||||
    try testing.expect(table.remove("foo") == null);
 | 
			
		||||
    try table.insert("foo", foo);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user