Go Custom Allocator Challenge
Go's garbage collector is highly efficient, but sometimes you need more control over memory management for performance-critical applications or to interact with C code. This challenge asks you to build a simple custom memory allocator in Go. You'll learn how Go's runtime interacts with memory and how to manage it directly.
Problem Description
You need to implement a CustomAllocator struct that provides methods for allocating and deallocating blocks of memory. This allocator should manage a pre-allocated contiguous buffer of memory.
What needs to be achieved:
- Create a
CustomAllocatorthat initializes with a fixed-size byte slice (the memory pool). - Implement an
Allocate(size int)method that returns a pointer (represented as an[]byteslice pointing into the memory pool) to a block of memory of the requestedsize. - Implement a
Deallocate(ptr []byte)method that marks the memory block referenced byptras free. - The allocator should keep track of free and used memory blocks.
- Handle cases where allocation fails due to insufficient memory.
Key requirements:
- Memory Pool: The allocator uses a single, contiguous byte slice as its memory pool.
- Allocation Strategy: A simple "first-fit" allocation strategy is sufficient: find the first free block that is large enough to satisfy the allocation request.
- Deallocation: Deallocating a block should make that memory available for future allocations. You do not need to implement memory coalescing (merging adjacent free blocks) for this challenge, but consider how simple deallocation would work.
- No Garbage Collection Interference: Your allocator should operate on raw bytes and not rely on Go's garbage collector to manage the allocated memory itself. The GC will manage the
CustomAllocatorstruct and the underlying memory pool slice, but the slices returned byAllocateshould represent direct memory access within that pool.
Expected behavior:
Allocateshould return a valid slice of the requested size if memory is available.Allocateshould return an error (e.g.,fmt.Errorf("not enough memory")) if the requested size cannot be accommodated.Deallocateshould correctly mark the memory as free, allowing it to be reused. CallingDeallocateon a pointer not originally returned byAllocate(or already deallocated) can be considered undefined behavior, but a robust implementation might have checks for this.- Multiple calls to
AllocateandDeallocateshould work correctly, reusing freed memory.
Important edge cases:
- Allocating 0 bytes.
- Allocating memory larger than the entire pool.
- Attempting to deallocate a nil slice.
- Deallocating a slice that has already been deallocated.
Examples
Example 1:
Input:
allocator := NewCustomAllocator(1024) // 1KB memory pool
ptr1, err1 := allocator.Allocate(100)
ptr2, err2 := allocator.Allocate(200)
allocator.Deallocate(ptr1)
ptr3, err3 := allocator.Allocate(150)
Output:
ptr1: non-nil slice of length 100
err1: nil
ptr2: non-nil slice of length 200
err2: nil
ptr3: non-nil slice of length 150 (ideally reusing memory from ptr1)
err3: nil
Explanation:
The allocator is initialized with 1024 bytes. The first allocation of 100 bytes succeeds. The second allocation of 200 bytes also succeeds, leaving 1024 - 100 - 200 = 724 bytes free. Deallocating ptr1 frees up 100 bytes. The third allocation of 150 bytes can then be satisfied, possibly by reusing the memory block previously occupied by ptr1.
Example 2:
Input:
allocator := NewCustomAllocator(200) // 200 byte memory pool
ptr1, _ := allocator.Allocate(150)
ptr2, _ := allocator.Allocate(100) // This allocation should fail
Output:
ptr1: non-nil slice of length 150
ptr2: nil slice
Error from second allocation: "not enough memory"
Explanation:
After allocating 150 bytes from a 200-byte pool, only 50 bytes remain. An attempt to allocate 100 bytes fails, as there isn't a contiguous block of 100 free bytes.
Constraints
- The total memory pool size will be between 1 byte and 1,000,000 bytes (inclusive).
- The requested allocation size will be between 0 bytes and 1,000,000 bytes (inclusive).
- The
CustomAllocatorshould be thread-safe forAllocateandDeallocateoperations (consider usingsync.Mutex). - The implementation should aim for reasonable performance; frequent reallocations or inefficient searching for free blocks should be avoided.
Notes
- You'll need to devise a way to track free and used memory blocks within the contiguous buffer. A common approach is to use metadata stored alongside the memory blocks themselves or a separate data structure (like a linked list of free blocks).
- Consider how you will represent the "pointer" returned by
Allocate. In Go, a[]byteslice pointing into the memory pool is a good representation as it carries both a pointer and a length. - Think about the overhead of your metadata. For small allocations, metadata can become a significant portion of the memory used.
- For this challenge, simply returning an error on allocation failure is sufficient. You are not required to implement resizing or compaction.
- The
NewCustomAllocatorfunction should return a pointer to aCustomAllocatorstruct.