# Why Allocations Cost Time

### Reducing Allocations

Allocations are one of the most common causes of slow programs.

An allocation asks an allocator for memory at runtime. In Zig, that usually looks like this:

```zig
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
```

This code is clear. It says exactly where memory comes from and where it is released. But clarity does not make allocation free. If this code runs once, it probably does not matter. If it runs millions of times, it can dominate the program.

## Why Allocations Cost Time

An allocator has to do real work.

It may need to:

- find a free block of memory
- record metadata
- maintain internal data structures
- handle alignment
- split or merge memory blocks
- ask the operating system for more pages
- synchronize between threads

A single allocation can be cheap. Many allocations can become expensive.

This is especially true for small, repeated allocations:

```zig
for (items) |item| {
    const temp = try allocator.alloc(u8, 64);
    defer allocator.free(temp);

    process(item, temp);
}
```

This allocates and frees memory once per item. If `items` has one million elements, the loop performs one million allocations.

## Allocation Frequency Matters

The first question is not only “how much memory do I allocate?”

It is also “how often do I allocate?”

These two programs both use memory, but they behave very differently.

Bad:

```zig
for (0..1000000) |_| {
    const buf = try allocator.alloc(u8, 1024);
    defer allocator.free(buf);

    use(buf);
}
```

Better:

```zig
const buf = try allocator.alloc(u8, 1024);
defer allocator.free(buf);

for (0..1000000) |_| {
    use(buf);
}
```

The second version allocates once and reuses the buffer.

That is usually much faster.

## Reuse Buffers

Buffer reuse is one of the simplest allocation optimizations.

Instead of creating temporary memory repeatedly, create it once and pass it around.

```zig
fn processMany(allocator: std.mem.Allocator, items: []const Item) !void {
    const scratch = try allocator.alloc(u8, 4096);
    defer allocator.free(scratch);

    for (items) |item| {
        try processOne(item, scratch);
    }
}
```

The scratch buffer belongs to `processMany`.

Each call to `processOne` reuses the same memory.

```zig
fn processOne(item: Item, scratch: []u8) !void {
    _ = item;
    _ = scratch;

    // Use scratch memory temporarily.
}
```

This style makes memory ownership easy to see.

## Prefer Stack Memory for Small Fixed Data

If the size is small and known at compile time, stack memory is often better.

Heap allocation:

```zig
const temp = try allocator.alloc(u8, 256);
defer allocator.free(temp);
```

Stack allocation:

```zig
var temp: [256]u8 = undefined;
```

The stack version needs no allocator.

It is simple and fast.

Then pass it as a slice:

```zig
try process(temp[0..]);
```

Use stack memory for small temporary buffers when the size is fixed and safe.

Do not put huge arrays on the stack. Large stack allocations can overflow the stack.

## Use ArrayList Carefully

`std.ArrayList` is useful for dynamic arrays.

```zig
var list = std.ArrayList(u32).init(allocator);
defer list.deinit();

try list.append(10);
try list.append(20);
```

But `append` may allocate when the list needs more capacity.

If you know the approximate size, reserve memory first:

```zig
try list.ensureTotalCapacity(1000);
```

Then many appends can happen without repeated growth allocations.

```zig
for (0..1000) |i| {
    try list.append(@intCast(i));
}
```

This reduces allocator pressure.

## Capacity vs Length

Dynamic containers usually have two important values:

| Term | Meaning |
|---|---|
| Length | Number of items currently stored |
| Capacity | Number of items that can fit before another allocation |

A list with length 3 and capacity 8 contains 3 items, but has room for 5 more.

Appending within capacity is cheap.

Appending beyond capacity may allocate.

This is why reserving capacity matters.

## Use `clearRetainingCapacity`

Sometimes you want to empty a list but keep its memory.

```zig
list.clearRetainingCapacity();
```

This sets the length to zero but keeps the allocated buffer.

That is useful inside loops:

```zig
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();

for (inputs) |input| {
    list.clearRetainingCapacity();

    try buildOutput(input, &list);
    try writeOutput(list.items);
}
```

This avoids allocating a fresh list for every input.

## Avoid Building Strings Repeatedly

String building often causes hidden allocation pressure.

Bad pattern:

```zig
for (items) |item| {
    var text = std.ArrayList(u8).init(allocator);
    defer text.deinit();

    try text.writer().print("item: {}\n", .{item.id});
    try output(text.items);
}
```

This creates a new dynamic buffer per item.

Better:

```zig
var text = std.ArrayList(u8).init(allocator);
defer text.deinit();

for (items) |item| {
    text.clearRetainingCapacity();

    try text.writer().print("item: {}\n", .{item.id});
    try output(text.items);
}
```

The buffer is reused.

## Use Arena Allocators for Batch Lifetimes

An arena allocator is useful when many allocations share the same lifetime.

Example:

```zig
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();

const arena_allocator = arena.allocator();
```

You allocate many objects from the arena:

```zig
const a = try arena_allocator.alloc(u8, 100);
const b = try arena_allocator.alloc(u32, 50);
```

Then release everything at once when the arena is destroyed.

This is useful for:

- parsing one file
- handling one request
- compiling one module
- loading one level in a game
- building temporary data for one operation

Arena allocation avoids many individual frees.

The tradeoff is that individual objects are not freed separately.

## Reset an Arena

For repeated batch work, you can reset an arena between batches.

```zig
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();

for (requests) |request| {
    _ = arena.reset(.retain_capacity);

    const aa = arena.allocator();

    try handleRequest(aa, request);
}
```

This keeps memory available for reuse while clearing the previous batch.

That can be much faster than allocating and freeing every object independently.

## Use Fixed Buffers

A fixed buffer allocator uses memory you provide.

```zig
var memory: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&memory);

const allocator = fba.allocator();
```

Now allocations come from `memory`.

```zig
const buf = try allocator.alloc(u8, 128);
```

This is useful when you want:

- no heap usage
- predictable memory limits
- temporary allocation inside a known buffer
- embedded-friendly behavior

When the fixed buffer is full, allocation fails.

That failure is explicit.

## Avoid Per-Element Heap Allocation

This is a common mistake.

Bad:

```zig
const User = struct {
    name: []u8,
};

for (users) |*user| {
    user.name = try allocator.dupe(u8, "anonymous");
}
```

This allocates separately for every name.

Sometimes that is necessary. Often, it is better to store data in one larger buffer or arena.

Better for batch data:

```zig
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();

const aa = arena.allocator();

for (users) |*user| {
    user.name = try aa.dupe(u8, "anonymous");
}
```

Now all names are released together.

## Store Inline Data When Reasonable

Sometimes a pointer causes an allocation that could be avoided.

Allocation-based design:

```zig
const SmallName = struct {
    bytes: []u8,
};
```

Inline design:

```zig
const SmallName = struct {
    len: u8,
    bytes: [32]u8,
};
```

The inline version can store short names without heap allocation.

This is useful when:

- most values are small
- maximum size is known
- fixed memory cost is acceptable

The tradeoff is wasted space for short values.

## Use Slices Instead of Copies

A slice is a view into existing memory.

Copying:

```zig
const copy = try allocator.dupe(u8, input);
```

Borrowing:

```zig
const view = input[start..end];
```

The slice does not allocate.

It points into the original memory.

This is excellent for parsers and text processing. Instead of copying every token, store slices into the original source buffer.

```zig
const Token = struct {
    text: []const u8,
};
```

This keeps tokenization cheap.

## Beware Slice Lifetimes

Slices avoid allocation, but they depend on the original memory staying alive.

Bad:

```zig
fn makeName() []const u8 {
    var buffer = [_]u8{ 'z', 'i', 'g' };
    return buffer[0..];
}
```

The slice points to stack memory that disappears when the function returns.

That is invalid.

Allocation reduction must not break lifetimes.

## Measure Allocations

You can often find allocation problems by counting them.

Questions to ask:

- How many allocations happen per request?
- How many allocations happen per file?
- How many allocations happen per frame?
- Which function allocates most often?
- Are temporary allocations reused?
- Are containers reserving capacity?

Zig helps because allocator use is explicit in function signatures.

If a function takes an allocator, it may allocate.

That is visible at the call site.

## Allocation-Free APIs

Some APIs should avoid allocation entirely.

Instead of returning a newly allocated result:

```zig
fn formatUser(allocator: std.mem.Allocator, user: User) ![]u8 {
    return try std.fmt.allocPrint(allocator, "user: {}", .{user.id});
}
```

you can write into a caller-provided buffer:

```zig
fn formatUser(buffer: []u8, user: User) ![]u8 {
    return try std.fmt.bufPrint(buffer, "user: {}", .{user.id});
}
```

The caller controls memory.

```zig
var buffer: [128]u8 = undefined;
const text = try formatUser(buffer[0..], user);
```

This avoids heap allocation.

It also makes failure explicit if the buffer is too small.

## API Design Matters

A performance-friendly Zig API often lets the caller choose memory strategy.

Good pattern:

```zig
fn parse(allocator: std.mem.Allocator, input: []const u8) !Document {
    // caller decides allocator
}
```

Even better when possible:

```zig
fn parseInto(output: *Document, scratch: []u8, input: []const u8) !void {
    // caller provides storage
}
```

The right design depends on the use case.

Simple APIs are easier to use.

Explicit storage APIs are faster and more predictable.

## Mental Model

Reducing allocations means moving memory decisions outward.

Instead of allocating deep inside small functions, prefer:

- caller-provided buffers
- reused containers
- reserved capacity
- stack memory for small fixed buffers
- arenas for batch lifetimes
- slices instead of copies
- fixed buffers for bounded memory

Allocations are not bad. Unnecessary repeated allocations are bad.

In Zig, memory is visible. Use that visibility to make allocation frequency low, predictable, and easy to control.

