allocators/pool

The pool allocator manages a collection of fixed-size memory blocks. All allocations are the same size, making this allocator extremely fast (O(1) for both allocation and deallocation) and completely free of fragmentation. Free blocks are tracked using an intrusive free list stored within the unused blocks themselves.

import "conjure/allocators/pool" as pool

When to Use Pool Allocators

Pool allocators are perfect when:

  • All allocations are the same size (same struct type)
  • Objects are created and destroyed frequently (spawn/despawn patterns)
  • You need O(1) allocation AND deallocation
  • Order of deallocation doesn’t matter

Common use cases:

  • Game entities (enemies, bullets, NPCs)
  • Particle systems
  • Network packet buffers
  • GUI widgets
  • Database connection handles

Creating a Pool

new

Creates a new pool allocator with the specified block size and count.

struct Enemy { x f32, y f32, health i32, flags u32 }

// Automatic heap allocation (most common)
var enemyPool = pool.new(Enemy.size, 1000)  // 1000 enemies
defer pool.destroy(&enemyPool)

// With provided buffer (you manage the memory)
var bufferSize = Enemy.size * 1000
var buffer = memory.alloc(bufferSize)
var enemyPool = pool.new(Enemy.size, 1000, buffer)
defer memory.free(buffer)

Signature:

func new(blockSize usize, blockCount usize, buffer rawptr = null) Pool

If buffer is null (the default), heap memory is allocated automatically.

newForType

Creates a pool for a specific type (convenience function).

struct Bullet { x f32, y f32, vx f32, vy f32, damage i32 }

var bulletPool = pool.newForType[Bullet](10000)  // 10000 bullets
defer pool.destroy(&bulletPool)

Signature:

func newForType[T](blockCount usize) Pool

destroy

Frees the backing memory of a pool created without a provided buffer.

pool.destroy(&enemyPool)

Signature:

func destroy(p *Pool)

Allocating Memory

alloc

Allocates a single block from the pool.

var enemy = *Enemy(pool.alloc(&enemyPool))
if enemy == null {
    io.println("Pool exhausted - too many enemies!")
    return
}
enemy.x = spawnX
enemy.y = spawnY
enemy.health = 100

Signature:

func alloc(p *Pool) rawptr

Returns null if all blocks are allocated (pool exhausted).

allocZeroed

Allocates a block and sets all bytes to zero.

var enemy = *Enemy(pool.allocZeroed(&enemyPool))
// All fields are zero-initialized

Signature:

func allocZeroed(p *Pool) rawptr

Freeing Memory

free

Returns a block to the pool (O(1) operation).

func killEnemy(enemy *Enemy) {
    enemy.health = 0
    pool.free(&enemyPool, enemy)  // Instant return to pool
}

Signature:

func free(p *Pool, ptr rawptr)

reset

Resets the pool, making all blocks available again.

// Start of new level - all enemies gone
pool.reset(&enemyPool)

Signature:

func reset(p *Pool)

This rebuilds the entire free list (O(n) operation).

Introspection

allocatedCount / freeCount / totalCount

Query pool state.

var active = pool.allocatedCount(&enemyPool)
var available = pool.freeCount(&enemyPool)
var total = pool.totalCount(&enemyPool)

io.println("Enemies: #{active}/#{total} (#{available} free)")

Signatures:

func allocatedCount(p *Pool) usize
func freeCount(p *Pool) usize
func totalCount(p *Pool) usize

blockSize

Returns the size of each block.

var size = pool.blockSize(&enemyPool)

Signature:

func blockSize(p *Pool) usize

isEmpty / isFull

Check pool state.

if pool.isFull(&bulletPool) {
    io.println("Warning: Bullet pool exhausted!")
}

if pool.isEmpty(&enemyPool) {
    io.println("All enemies defeated!")
}

Signatures:

func isEmpty(p *Pool) bool
func isFull(p *Pool) bool

utilization

Returns the percentage of blocks allocated (0.0 to 1.0).

var usage = pool.utilization(&particlePool)
if usage > 0.9 {
    io.println("Warning: Particle pool at #{usage * 100}% capacity!")
}

Signature:

func utilization(p *Pool) f32

Iteration

iterator / iterNext / iterGet

Iterate over all blocks in the pool (both allocated and free).

struct Entity { active bool, x f32, y f32 }

var iter = pool.iterator(&entityPool)
while pool.iterNext(&iter) {
    var entity = *Entity(pool.iterGet(&iter))
    if entity.active {
        updateEntity(entity)
    }
}

Signatures:

func iterator(p *Pool) PoolIterator
func iterNext(iter *PoolIterator) bool
func iterGet(iter *PoolIterator) rawptr

getBlock

Get a pointer to a specific block by index.

var firstEnemy = *Enemy(pool.getBlock(&enemyPool, 0))
var lastEnemy = *Enemy(pool.getBlock(&enemyPool, pool.totalCount(&enemyPool) - 1))

Signature:

func getBlock(p *Pool, index usize) rawptr

Types

Pool

The main pool allocator structure.

struct Pool {
    buffer rawptr          // Backing memory
    capacity usize         // Total buffer size
    blockSize usize        // Size of each block
    blockCount usize       // Total number of blocks
    freeList rawptr        // Head of free list
    allocatedCount usize   // Currently allocated
}

PoolIterator

Iterator for walking through all blocks.

struct PoolIterator {
    pool *Pool
    currentIndex usize
}

Example: Game Entity Pool

import "conjure/allocators/pool" as pool
import "conjure/io"

struct Enemy {
    active bool
    x f32
    y f32
    vx f32
    vy f32
    health i32
    type u8
}

var enemyPool = pool.createForType[Enemy](500)

func spawnEnemy(x f32, y f32, type u8) *Enemy {
    var e = *Enemy(pool.alloc(&enemyPool))
    if e == null {
        return null  // Pool full
    }

    e.active = true
    e.x = x
    e.y = y
    e.vx = 0
    e.vy = 0
    e.health = getHealthForType(type)
    e.type = type
    return e
}

func killEnemy(e *Enemy) {
    e.active = false
    pool.free(&enemyPool, e)
}

func updateEnemies(dt f32) {
    var iter = pool.iterator(&enemyPool)
    while pool.iterNext(&iter) {
        var e = *Enemy(pool.iterGet(&iter))
        if e.active {
            e.x = e.x + e.vx * dt
            e.y = e.y + e.vy * dt

            if e.health <= 0 {
                killEnemy(e)
            }
        }
    }
}

func getEnemyStats() {
    io.println("Active enemies: #{pool.allocatedCount(&enemyPool)}")
    io.println("Pool usage: #{pool.utilization(&enemyPool) * 100}%")
}

Example: Particle System

import "conjure/allocators/pool" as pool

struct Particle {
    active bool
    x f32
    y f32
    vx f32
    vy f32
    life f32
    maxLife f32
    color u32
}

var particlePool = pool.createForType[Particle](50000)

func emitParticles(x f32, y f32, count i32) {
    for i in <count {
        var p = *Particle(pool.alloc(&particlePool))
        if p == null {
            break  // Pool exhausted
        }

        p.active = true
        p.x = x + randomRange(-5, 5)
        p.y = y + randomRange(-5, 5)
        p.vx = randomRange(-50, 50)
        p.vy = randomRange(-100, -50)
        p.life = randomRange(0.5, 2.0)
        p.maxLife = p.life
        p.color = 0xFFFFFFFF
    }
}

func updateParticles(dt f32) {
    var iter = pool.iterator(&particlePool)
    while pool.iterNext(&iter) {
        var p = *Particle(pool.iterGet(&iter))
        if p.active {
            p.x = p.x + p.vx * dt
            p.y = p.y + p.vy * dt
            p.vy = p.vy + 200 * dt  // Gravity
            p.life = p.life - dt

            if p.life <= 0 {
                p.active = false
                pool.free(&particlePool, p)
            }
        }
    }
}

Example: Object Recycling Pattern

import "conjure/allocators/pool" as pool

struct Bullet {
    active bool
    x f32
    y f32
    angle f32
    speed f32
    damage i32
}

var bulletPool = pool.createForType[Bullet](10000)

// Pre-warm the pool by allocating and freeing
func prewarmPool() {
    var ptrs [100]rawptr
    for i in <100 {
        ptrs[i] = pool.alloc(&bulletPool)
    }
    for i in <100 {
        pool.free(&bulletPool, ptrs[i])
    }
}

func fireBullet(x f32, y f32, angle f32) *Bullet {
    var b = *Bullet(pool.allocZeroed(&bulletPool))
    if b == null {
        // Pool exhausted - recycle oldest bullet
        recycleOldestBullet()
        b = *Bullet(pool.alloc(&bulletPool))
    }

    b.active = true
    b.x = x
    b.y = y
    b.angle = angle
    b.speed = 500
    b.damage = 10
    return b
}

Block Size Requirements

Pool blocks must be at least 8 bytes (the size of a pointer) because free blocks store the free list pointer internally.

struct TinyData { value u8 }  // Only 1 byte!

// This will automatically use 8-byte blocks
var tinyPool = pool.createForType[TinyData](1000)
// Each "TinyData" uses 8 bytes of pool memory, not 1

If you have very small objects, consider packing multiple into a larger struct:

struct TinyDataPacked {
    values [8]u8  // 8 tiny values in one block
}