|
| 1 | +//! Let's say that we want to store a list of `struct {a: u8, b: u16}`s. |
| 2 | +//! |
| 3 | +//! We could use a normal ArrayList; we'd call this an Array of Structs (AoS) design, |
| 4 | +//! though in Zig "Slice of Structs" would be a more correct name, because we'd be |
| 5 | +//! storing a list where each entry in the list stores the whole struct contiguously. |
| 6 | +//! |
| 7 | +//! Alternatively, we could use MultiArrayList, where the list is split into |
| 8 | +//! `Struct.fields.len` segments, each storing all the values of a certain field |
| 9 | +//! contiguously. We'd call this a Struct of Arrays (SoA) design, which is |
| 10 | +//! also a misnomer when applied to Zig. :^) |
| 11 | +//! |
| 12 | +//! This is a little odd to wrap one's head around at first, so here's a diagram |
| 13 | +//! of how each design looks in memory with the example struct mentioned above: |
| 14 | +//! |
| 15 | +//! ArrayList (AoS): { {aaaaaaaabbbbbbbbbbbbbbbb} {aaaaaaaabbbbbbbbbbbbbbbb} {aaaaaaaabbbbbbbbbbbbbbbb} } |
| 16 | +//! MultiArrayList (SoA): { {aaaaaaaa} {aaaaaaaa} {aaaaaaaa} {bbbbbbbbbbbbbbbb} {bbbbbbbbbbbbbbbb} {bbbbbbbbbbbbbbbb} } |
| 17 | +//! |
| 18 | +//! Both example lists show three entries and each letter represents a bit of memory for |
| 19 | +//! that field. Note that this diagram is simplified, and that we do not represent |
| 20 | +//! any padding that may be introduced by the compiler. |
| 21 | +//! |
| 22 | +//! When to use: |
| 23 | +//! - You only access certain fields most of the time, and loading |
| 24 | +//! the entire struct would be a waste |
| 25 | +//! - You want to help the compiler autovectorize / perform some |
| 26 | +//! manual vectorization using Zig's SIMD features |
| 27 | +//! |
| 28 | +//! When not to use: |
| 29 | +//! - If you're always accessing every field in your list entries, |
| 30 | +//! then you won't find much use in MultiArrayLists |
| 31 | + |
| 32 | +const std = @import("std"); |
| 33 | + |
| 34 | +pub const Player = struct { |
| 35 | + name: []const u8, |
| 36 | + health: u16, |
| 37 | +}; |
| 38 | + |
| 39 | +const PlayerList = std.MultiArrayList(Player); |
| 40 | + |
| 41 | +test { |
| 42 | + const allocator = std.testing.allocator; |
| 43 | + |
| 44 | + var players = PlayerList{}; |
| 45 | + defer players.deinit(allocator); |
| 46 | + |
| 47 | + try players.append(allocator, .{ .name = "Auguste", .health = 100 }); |
| 48 | + try players.append(allocator, .{ .name = "Techatrix", .health = 125 }); |
| 49 | + try players.append(allocator, .{ .name = "ZLS Premium User", .health = 50_000 }); |
| 50 | + |
| 51 | + // We're about to call `.items(...)` a bunch, |
| 52 | + // so we can save some performance by |
| 53 | + // calling `.slice()` and calling `.items(...)` |
| 54 | + // on that instead. |
| 55 | + var slice = players.slice(); |
| 56 | + |
| 57 | + // I just want to increase every player's health! |
| 58 | + // This pattern allows LLVM to easily autovectorize |
| 59 | + // this modification code; I challenge you to look |
| 60 | + // at this in Compiler Explorer! :) |
| 61 | + for (slice.items(.health)) |*health| { |
| 62 | + health.* += 10; |
| 63 | + } |
| 64 | + |
| 65 | + // Let's access both fields and see where our players' |
| 66 | + // health is at now! |
| 67 | + for (slice.items(.name), slice.items(.health)) |name, health| { |
| 68 | + std.debug.print("{s} has {d} health!\n", .{ name, health }); |
| 69 | + } |
| 70 | +} |
0 commit comments