The free list allocator is a general-purpose allocator that supports variable-sized allocations and out-of-order deallocation. It maintains a linked list of free memory blocks and supports coalescence (merging adjacent free blocks) to reduce fragmentation.
import "conjure/allocators/freelist" as freelistWhen to Use Free List Allocators
Free list allocators are appropriate when:
- Allocation sizes vary (different structs, variable-length data)
- You need to free in any order (unpredictable lifetimes)
- You can’t use specialized allocators (requirements don’t fit)
Common use cases:
- Asset loading (textures, models of various sizes)
- Dynamic data structures (trees, graphs)
- Plugin/extension memory management
- Fallback allocator when patterns are unknown
Creating a Free List
new
Creates a new free list allocator with the specified capacity.
// Automatic heap allocation (most common)
var fl = freelist.new(4 * 1024 * 1024) // 4 MB
defer freelist.destroy(&fl)
// With provided buffer (you manage the memory)
var buffer = memory.alloc(4 * 1024 * 1024)
var fl = freelist.new(4 * 1024 * 1024, buffer)
defer memory.free(buffer)
// With best-fit placement policy
var fl = freelist.new(4 * 1024 * 1024, null, PlacementPolicy.BestFit)
defer freelist.destroy(&fl)Signature:
func new(capacity usize, buffer rawptr = null, policy PlacementPolicy = PlacementPolicy.FirstFit) FreeListIf buffer is null (the default), heap memory is allocated automatically.
destroy
Frees the backing memory of a free list created without a provided buffer.
freelist.destroy(&fl)Signature:
func destroy(fl *FreeList)Placement Policies
The free list supports two strategies for selecting which free block to use:
FirstFit
Uses the first free block that’s large enough. Fast allocation but may cause more fragmentation.
var fl = freelist.createWithPolicy(size, freelist.PlacementPolicy.FirstFit)BestFit
Scans all free blocks to find the smallest one that fits. Slower allocation but reduces fragmentation.
var fl = freelist.createWithPolicy(size, freelist.PlacementPolicy.BestFit)Signatures:
enum PlacementPolicy {
FirstFit // Fast, more fragmentation
BestFit // Slower, less fragmentation
}Allocating Memory
alloc
Allocates a block of the requested size with default alignment.
var data = freelist.alloc(&fl, 1024)
if data == null {
io.println("Out of memory!")
}Signature:
func alloc(fl *FreeList, size usize) rawptrReturns null if there’s no suitable free block.
allocAligned
Allocates memory with a specific alignment.
var aligned = freelist.allocAligned(&fl, 256, 16)Signature:
func allocAligned(fl *FreeList, size usize, alignment usize) rawptrallocZeroed
Allocates memory and sets all bytes to zero.
var zeroed = freelist.allocZeroed(&fl, 512)Signature:
func allocZeroed(fl *FreeList, size usize) rawptrallocType
Allocates memory for a specific type.
struct Asset { type u8, size usize, data rawptr }
var asset = freelist.allocType[Asset](&fl)Signature:
func allocType[T](fl *FreeList) rawptrFreeing Memory
free
Frees a previously allocated block. The block is added to the free list and merged with adjacent free blocks if possible (coalescence).
freelist.free(&fl, data)Signature:
func free(fl *FreeList, ptr rawptr)reset
Resets the free list, making all memory available as one large block.
freelist.reset(&fl)Signature:
func reset(fl *FreeList)Resizing Allocations
resize
Attempts to resize an allocation. If it can’t grow in place, allocates new memory and copies the data.
var data = freelist.alloc(&fl, 100)
data = freelist.resize(&fl, data, 100, 200)
if data == null {
io.println("Not enough memory to resize!")
}Signature:
func resize(fl *FreeList, ptr rawptr, oldSize usize, newSize usize) rawptrIntrospection
used / capacity / freeSpace
Query memory usage.
var bytesUsed = freelist.used(&fl)
var total = freelist.capacity(&fl)
var available = freelist.freeSpace(&fl)Signatures:
func used(fl *FreeList) usize
func capacity(fl *FreeList) usize
func freeSpace(fl *FreeList) usizefreeBlockCount
Returns the number of separate free blocks. More blocks generally means more fragmentation.
var blocks = freelist.freeBlockCount(&fl)
io.println("Free list has #{blocks} fragments")Signature:
func freeBlockCount(fl *FreeList) usizelargestFreeBlock
Returns the size of the largest available block. This is the maximum allocation size currently possible.
var maxAlloc = freelist.largestFreeBlock(&fl)
if size > maxAlloc {
io.println("Cannot allocate #{size} bytes, max is #{maxAlloc}")
}Signature:
func largestFreeBlock(fl *FreeList) usizeisEmpty
Returns true if all memory is allocated (no free blocks).
if freelist.isEmpty(&fl) {
io.println("Free list exhausted!")
}Signature:
func isEmpty(fl *FreeList) boolfragmentation
Returns a fragmentation ratio from 0.0 (no fragmentation) to 1.0 (heavily fragmented).
Calculated as: 1 - (largest_block / total_free)
var frag = freelist.fragmentation(&fl)
if frag > 0.5 {
io.println("Warning: Memory is #{frag * 100}% fragmented!")
}Signature:
func fragmentation(fl *FreeList) f32Types
FreeList
The main free list allocator structure.
struct FreeList {
buffer rawptr // Backing memory
capacity usize // Total size
head *FreeNode // First free block
used usize // Bytes allocated
policy PlacementPolicy // Allocation strategy
}FreeNode
Internal structure representing a free block.
struct FreeNode {
size usize // Block size
next *FreeNode // Next free block
}AllocationHeader
Internal header stored before each allocation.
struct AllocationHeader {
blockSize usize // Total block size
padding usize // Padding bytes
}Example: Asset Manager
import "conjure/allocators/freelist" as freelist
import "conjure/io"
var assetMemory = freelist.create(64 * 1024 * 1024) // 64 MB for assets
struct Asset {
name string
type u8
dataSize usize
data rawptr
}
func loadAsset(path string) *Asset {
var size = getAssetSize(path)
// Allocate header
var asset = *Asset(freelist.allocType[Asset](&assetMemory))
if asset == null {
io.println("Out of asset memory!")
return null
}
// Allocate data
asset.data = freelist.alloc(&assetMemory, size)
if asset.data == null {
freelist.free(&assetMemory, asset)
io.println("Out of asset memory for data!")
return null
}
asset.name = path
asset.dataSize = size
readFile(path, asset.data)
return asset
}
func unloadAsset(asset *Asset) {
freelist.free(&assetMemory, asset.data)
freelist.free(&assetMemory, asset)
}
func getAssetMemoryStats() {
var used = freelist.used(&assetMemory)
var total = freelist.capacity(&assetMemory)
var frag = freelist.fragmentation(&assetMemory)
io.println("Asset memory: #{used / 1024}KB / #{total / 1024}KB")
io.println("Fragmentation: #{frag * 100}%")
io.println("Largest allocatable: #{freelist.largestFreeBlock(&assetMemory) / 1024}KB")
}Example: Dynamic String Buffer
import "conjure/allocators/freelist" as freelist
var stringMemory = freelist.create(1024 * 1024) // 1 MB for strings
struct DynamicString {
data rawptr
length usize
capacity usize
}
func newString(initial string) DynamicString {
var cap = initial.len + 32 // Some extra space
var data = freelist.alloc(&stringMemory, cap)
if data != null {
memory.copy(data, initial.data, initial.len)
}
return DynamicString{
data = data,
length = initial.len,
capacity = cap,
}
}
func appendString(s *DynamicString, text string) {
var newLen = s.length + text.len
if newLen > s.capacity {
// Need to grow
var newCap = newLen * 2
s.data = freelist.resize(&stringMemory, s.data, s.capacity, newCap)
s.capacity = newCap
}
memory.copy(s.data[s.length], text.data, text.len)
s.length = newLen
}
func freeString(s *DynamicString) {
freelist.free(&stringMemory, s.data)
s.data = null
s.length = 0
s.capacity = 0
}Example: Plugin Memory Isolation
import "conjure/allocators/freelist" as freelist
// Each plugin gets its own memory region
struct PluginMemory {
allocator freelist.FreeList
name string
}
func createPluginMemory(name string, size usize) PluginMemory {
return PluginMemory{
allocator = freelist.create(size),
name = name,
}
}
func destroyPluginMemory(pm *PluginMemory) {
freelist.destroy(&pm.allocator)
}
// Plugin API
func pluginAlloc(pm *PluginMemory, size usize) rawptr {
return freelist.alloc(&pm.allocator, size)
}
func pluginFree(pm *PluginMemory, ptr rawptr) {
freelist.free(&pm.allocator, ptr)
}
func getPluginMemoryUsage(pm *PluginMemory) f32 {
var used = freelist.used(&pm.allocator)
var total = freelist.capacity(&pm.allocator)
return f32(used) / f32(total)
}Fragmentation and Coalescence
The free list automatically coalesces (merges) adjacent free blocks when you free memory:
Before free(B):
[A used][B used][C free][D used]
After free(B):
[A used][B+C free........][D used]This reduces fragmentation over time, but heavy allocation/deallocation patterns can still fragment memory.
Monitoring Fragmentation
func checkFragmentation(fl *freelist.FreeList) {
var frag = freelist.fragmentation(fl)
var blocks = freelist.freeBlockCount(fl)
if frag > 0.7 or blocks > 100 {
io.println("Warning: High fragmentation detected!")
io.println(" Fragmentation: #{frag * 100}%")
io.println(" Free blocks: #{blocks}")
io.println(" Consider resetting or defragmenting")
}
}Defragmentation Strategy
If fragmentation becomes severe, the simplest solution is to:
- Allocate a new free list
- Copy live data to the new allocator
- Destroy the old allocator
func defragmentAssets(assets []Asset) {
var newMemory = freelist.create(freelist.capacity(&assetMemory))
for asset in assets {
var newData = freelist.alloc(&newMemory, asset.dataSize)
memory.copy(newData, asset.data, asset.dataSize)
asset.data = newData
}
freelist.destroy(&assetMemory)
assetMemory = newMemory
}Performance Considerations
| Operation | Time Complexity |
|---|---|
| alloc (first-fit) | O(n) |
| alloc (best-fit) | O(n) |
| free | O(n) for sorted insert |
| reset | O(1) |
Where n = number of free blocks.
For performance-critical code, prefer: