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 poolWhen 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) PoolIf 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) Pooldestroy
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 = 100Signature:
func alloc(p *Pool) rawptrReturns 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-initializedSignature:
func allocZeroed(p *Pool) rawptrFreeing 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) usizeblockSize
Returns the size of each block.
var size = pool.blockSize(&enemyPool)Signature:
func blockSize(p *Pool) usizeisEmpty / 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) boolutilization
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) f32Iteration
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) rawptrgetBlock
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) rawptrTypes
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 1If you have very small objects, consider packing multiple into a larger struct:
struct TinyDataPacked {
values [8]u8 // 8 tiny values in one block
}