索引,而非指针
Indices, not Pointers

原始链接: https://joegm.github.io/blog/indices-not-pointers/

## 数据结构中的索引与指针 Zig 中一种强大的优化技术,灵感来源于 Andrew Kelley 的数据导向设计演讲,涉及在树等数据结构中使用索引代替指针。传统上,基于节点的结构为每个节点单独分配内存并用指针链接它们。这种方法虽然常见,但会因单独分配和更大的内存占用(64 位系统上每个指针 8 字节)而产生开销。 另一种方法是将所有节点存储在连续的动态数组(如 `ArrayList`)中,并使用索引(可能为 4 字节)引用它们。这种方法可以减少内存使用量,由于连续性提高缓存局部性,并加快访问速度。此外,释放整个结构成为一个单一操作,从而大大提高性能。 虽然从数组中删除单个节点效率低下,但通常是不必要的——许多结构(如抽象语法树 (AST))可以完全释放。对于需要删除单个节点的情况,可以实现一个空闲列表来管理数组中的可用插槽。这种技术在 Zig 示例中得到演示,通过最大限度地减少分配开销和最大限度地提高内存效率,可以带来显著的性能提升。

相关文章

原文

Jul 15, 2025

There is a pattern I’ve learned while using Zig which I’ve never seen used in any other language. It’s an extremely simple trick which - when applied to a data structure - reduces memory usage, reduces memory allocations, speeds up accesses, makes freeing instantaneous, and generally makes everything much, much faster. The trick is to use indices, not pointers.

This is something I learned from a talk by Andrew Kelley (Zig’s creator) on data-oriented design. It’s used in Zig’s compiler to make very memory-efficient ASTs, and can be applied to pretty much any node-based data structure, usually trees.

So what does this mean exactly? Well, to use indices means to store the nodes of the data structure in a dynamic array, appending new nodes instead of individually allocating them. Nodes can then reference each other via indices instead of pointers.

A comparison of memory layouts with different storage methods

Pretty simple, right? But this strategy has some major performance benefits.

A pointer costs 8 bytes to store on a modern 64-bit system, but unless your planning on storing over 4 billion nodes in memory, an index can be stored in just 4 bytes.

Due to the reduced node size and the fact that nodes are stored contiguously in memory, the data structure will fit into fewer memory pages and more nodes will fit in the cpu’s cache line, which generally improves access times significantly.

The way most people learn to implement data structures like trees is to make a separate allocation for each individual node, one at a time. This is a very naive way of allocating memory, however, as each memory allocation comes with a small but significant overhead which can really slow things down for a large number of nodes. Storing nodes in a growable arraylist minimizes this overhead as arraylists grow superlinearly (e.g, doubling in size each time more space is needed) meaning the majority of new nodes can just be placed in the next available slot without requesting more memory!

An arraylist growing by moving elements to a bigger allocation

Freeing structures which are allocated in the traditional “nest of pointers” fashion can be very slow, as the entire structure has to be traversed to find and individually free each node. Storing nodes in a single allocation eliminates this problem entirely and freeing the structure becomes just a single free call, as it should be.

One disadvantage of storing all the nodes in a contiguous buffer is that it makes it harder to free an individual node as removing a single element from an arraylist would involve shifting over all the elements after it, a linear time operation which is almost always too slow to be practical. In practice this isn’t something you normally need to do as many data structures, like an AST, can be freed all at once, but if you need to be able to free individual nodes and still want to use this technique then the obvious solution would be to use a freelist.

Freelists

A freelist is, as the name suggests, a list used to track free slots in memory allocators. In our case we can simply use a stack to store indices of free slots in our arraylist and attempt to pop off this stack any time we add a new element. The extra code complexity should be weighed against the actual performance benefit when considering this approach.

A node allocation using a freelist

Here is a short demo of this technique in Zig (v0.14.1). There are some Zig quirks involved like passing memory allocators and using an enum as an index type but hopefully the general idea is clear.

pub fn main() !void {
    var debug_allocator = std.heap.DebugAllocator(.{}).init;
    defer _ = debug_allocator.deinit();

    var tree = Tree{
        
        .nodes = ArrayList(Tree.Node).init(debug_allocator.allocator()),
    };
    defer tree.nodes.deinit();

    
    const root = try tree.createNode(45);

    const a = try tree.createNode(-10);
    const b = try tree.createNode(89000);
    const c = try tree.createNode(2);

    tree.setLeftChild(root, a);
    tree.setRightChild(root, b);
    tree.setLeftChild(b, c);

    printTree(&tree);
}

const Tree = struct {
    
    nodes: ArrayList(Node),

    const Node = struct {
        data: i32,
        left_child: NodeIndex = .none,
        right_child: NodeIndex = .none,
    };

    
    
    const NodeIndex = enum(u32) {
        
        none = 0,
        _,
    };

    fn createNode(tree: *Tree, value: i32) std.mem.Allocator.Error!NodeIndex {
        const index: NodeIndex = @enumFromInt(@as(u32, @intCast(tree.nodes.items.len)));
        try tree.nodes.append(.{ .data = value });
        return index;
    }

    fn setLeftChild(tree: *const Tree, parent: NodeIndex, child: NodeIndex) void {
        tree.nodes.items[@intFromEnum(parent)].left_child = child;
    }

    fn setRightChild(tree: *const Tree, parent: NodeIndex, child: NodeIndex) void {
        tree.nodes.items[@intFromEnum(parent)].right_child = child;
    }
};

fn printTree(tree: *const Tree) void {
    assert(tree.nodes.items.len > 0);

    
    printNode(tree, @enumFromInt(0), 0);
}

fn printNode(tree: *const Tree, node_index: Tree.NodeIndex, depth: u32) void {
    const node = tree.nodes.items[@intFromEnum(node_index)];

    for (0..depth) |_| print("  ", .{});
    print("[{d}] {d}\n", .{ @intFromEnum(node_index), node.data });

    if (node.left_child != .none) printNode(tree, node.left_child, depth + 1);
    if (node.right_child != .none) printNode(tree, node.right_child, depth + 1);
}

const std = @import("std");
const ArrayList = std.ArrayList;
const assert = std.debug.assert;
const print = std.debug.print;

And here is the output:

$ zig run indices.zig
[0] 45
  [1] -10
  [2] 89000
    [3] 2
联系我们 contact @ memedata.com