Hone logo
Hone
Problems

Go Pool Allocator Challenge

You're tasked with building a memory pool allocator in Go. This type of allocator is useful for managing fixed-size chunks of memory efficiently, reducing the overhead of frequent memory allocations and deallocations for objects of the same size.

Problem Description

Implement a PoolAllocator in Go that can allocate and release fixed-size memory blocks. The allocator should maintain a pool of pre-allocated memory, and when a request for a block is made, it should return a slice pointing to an available block within the pool. When a block is no longer needed, it should be returned to the pool for reuse.

Key Requirements:

  • Fixed-Size Blocks: The allocator must be initialized with a specific blockSize and poolSize (number of blocks). All allocations will be of blockSize bytes.
  • Allocation: The Allocate() method should return a []byte slice representing an available memory block. If the pool is exhausted, it should return nil or an error.
  • Deallocation: The Release(block []byte) method should return a previously allocated []byte block back to the pool. It should handle cases where the provided block is not part of the pool or has already been released.
  • Thread-Safety: The allocator must be safe for concurrent use by multiple goroutines.
  • Efficiency: Minimize memory fragmentation and allocation/deallocation overhead.

Expected Behavior:

  1. Initialize the pool with a certain number of blocks of a fixed size.
  2. When Allocate() is called, return a pointer to a valid, unused block from the pool.
  3. When Release() is called with a valid block, mark that block as available again.
  4. If Allocate() is called and no blocks are available, it should indicate this (e.g., return nil).
  5. If Release() is called with an invalid block (e.g., a block not from this pool, or already released), it should be handled gracefully (e.g., logged, ignored, or return an error).

Edge Cases:

  • Releasing a block that has already been released.
  • Releasing a block that was not allocated by this allocator.
  • Attempting to allocate when the pool is full.
  • Initializing the allocator with zero or negative blockSize or poolSize.

Examples

Example 1: Basic Allocation and Deallocation

// Assume PoolAllocator is initialized as:
// allocator := NewPoolAllocator(16, 10) // 10 blocks of 16 bytes each

block1 := allocator.Allocate() // Returns a []byte of length 16
block2 := allocator.Allocate() // Returns another []byte of length 16

// ... use block1 and block2 ...

allocator.Release(block1) // Returns block1 to the pool
allocator.Release(block2) // Returns block2 to the pool

Explanation: Two blocks of 16 bytes are successfully allocated. After use, they are released back to the pool.

Example 2: Pool Exhaustion

// Assume PoolAllocator is initialized as:
// allocator := NewPoolAllocator(8, 3) // 3 blocks of 8 bytes each

blockA := allocator.Allocate() // Valid allocation
blockB := allocator.Allocate() // Valid allocation
blockC := allocator.Allocate() // Valid allocation

exhaustedBlock := allocator.Allocate() // Returns nil (or an error)

Explanation: All available blocks (3 of them) are allocated. A subsequent Allocate() call fails because the pool is empty.

Example 3: Releasing an Invalid Block

// Assume PoolAllocator is initialized as:
// allocator := NewPoolAllocator(32, 5)

blockX := allocator.Allocate()
invalidBlock := make([]byte, 32) // A block not from the pool

allocator.Release(blockX) // Valid release

// The behavior for releasing `invalidBlock` depends on implementation choice:
// - It might be ignored.
// - It might log an error.
// - It might return an error.
// A robust implementation should handle this without crashing.

Explanation: Releasing a block that was not allocated by this specific PoolAllocator instance should be handled safely without causing panics or corrupting the pool's state.

Constraints

  • blockSize: Must be a positive integer (e.g., 1 <= blockSize <= 1024).
  • poolSize: Must be a positive integer (e.g., 1 <= poolSize <= 1000).
  • The PoolAllocator must handle up to 100 concurrent Allocate() and Release() operations without data races or deadlocks.
  • Memory allocated by the pool should be contiguous []byte slices.

Notes

  • Consider how you will manage the underlying raw memory for the pool. You might want to pre-allocate a large buffer and then carve out fixed-size blocks from it.
  • A common approach for managing available blocks is to use a free list or a similar data structure.
  • Think about how to efficiently track which blocks are in use and which are available.
  • Thread safety can be achieved using Go's synchronization primitives like sync.Mutex or sync.Pool. However, since you're building a custom pool, you'll likely use sync.Mutex to protect your internal data structures.
  • The Release method should ideally check if the provided []byte slice is actually a valid block that was allocated by this pool instance. This can be tricky and might involve some internal bookkeeping. For this challenge, a basic check might suffice (e.g., ensuring it's within the pool's memory range).
Loading editor...
go