|
| 1 | +# small-array-list |
| 2 | + |
| 3 | +This library provides a `SmallArrayList` type (and some variants), which is |
| 4 | +optimized for memory efficient storage of arrays that generally contain few |
| 5 | +items. This works by sharing the memory internal storage space between either |
| 6 | +a fixed size array, or an externally allocated slice, and switching between |
| 7 | +the two as needed. See the [Small Capacity Limit](#small-capacity-limit) for |
| 8 | +details on this storage mechanism. |
| 9 | + |
| 10 | +```zig |
| 11 | +var list: SmallArrayList(i32) = .empty; |
| 12 | +defer list.deinit(allocator); |
| 13 | +
|
| 14 | +list.append(allocator, 1); |
| 15 | +list.append(allocator, 2); |
| 16 | +list.append(allocator, 3); |
| 17 | +
|
| 18 | +std.debug.print("len={} capacity={}\n", .{ list.len, list.capacity }); |
| 19 | +// On 64-bit systems, this will be: len=3 capacity=4 |
| 20 | +
|
| 21 | +for (list.items()) |item| { |
| 22 | + std.debug.print("item={}\n", .{ item }); |
| 23 | +} |
| 24 | +
|
| 25 | +if (!list.hasAllocation()) { |
| 26 | + std.debug.print("no allocations!\n", .{}); |
| 27 | +} |
| 28 | +``` |
| 29 | + |
| 30 | +> [!IMPORTANT] |
| 31 | +> Instances keep much of the interface in common with `std.array_list.ArrayListUnmanaged` |
| 32 | +> with a few important differences: |
| 33 | +> |
| 34 | +> 1. To get the number of items in the list, use `list.len` instead of `list.items.len`. |
| 35 | +> 1. To get the items of in the list, use `list.items()` instead of `list.items`. |
| 36 | +
|
| 37 | +## Small Capacity Limit |
| 38 | + |
| 39 | +The available small capacity limit is determined by the type stored and the |
| 40 | +native machine word size. Because a zig slice requires two machine words for |
| 41 | +storing it's `ptr` and `len`, this space can be used for direct item storage |
| 42 | +instead until the small capacity is exceeded. For any given type `T`, the |
| 43 | +small array list can store up to `2*@sizeOf(usize) / @sizeOf(T)` items before |
| 44 | +requiring any internal allocation. |
| 45 | + |
| 46 | +For example on a 64-bit processor will print out `smallCapacity=4 sizeOf=24`: |
| 47 | + |
| 48 | +```zig |
| 49 | +const List = SmallArrayList(i32); |
| 50 | +std.debug.print("smallCapacity={} sizeOf={}\n", .{List.smallCapacity, @sizeOf(List)}); |
| 51 | +``` |
| 52 | + |
| 53 | +The over size of a `SmallArrayList` is generally three machine words. This is |
| 54 | +achieved using a few trade-offs: |
| 55 | + |
| 56 | +1. The overlapped memory of the internal array and external slice is achieved |
| 57 | +using a union. By ensuring this union is indiscriminate, the array list can |
| 58 | +maximize storage efficiency. This union is then discriminated using the |
| 59 | +`capacity` field of the array list. Using `SmallArrayListSized`, you can set |
| 60 | +a small capacity limit that exceeds the default size. This will cause the |
| 61 | +overall small array list size to grow. |
| 62 | + |
| 63 | +2. The `len` and `capacity` are each half of a `usize`. This does mean it will result |
| 64 | +in an `error.OutOfMemory` when trying to allocate a capacity greater than |
| 65 | +half the maximum of a `std.array_list.ArrayList`. However, this library is |
| 66 | +optimized for small array lists. If lists of such size are needed, the standard |
| 67 | +library should be used. |
| 68 | + |
| 69 | +3. All `SmallArrayList` types are unmanaged, meaning they do not store the |
| 70 | +`std.mem.Allocator` internally, and each function that could possibly allocate |
| 71 | +or deallocate takes the allocator as a parameter. Note that the same allocator |
| 72 | +instance must be used for each call into any single small array list. |
| 73 | + |
| 74 | +> [!IMPORTANT] |
| 75 | +> One important thing to keep in mind when using larger types is that there is |
| 76 | +> a minimum small capacity of 1, so if the size of `T` exceeds two machine words, |
| 77 | +> the overall size of the `SmallArrayList` will expand. This can still be |
| 78 | +> beneficial, but it is something you'll want to consider. |
| 79 | +
|
| 80 | +## Variants |
| 81 | + |
| 82 | +The `SmallArrayList` uses the default alignment of `T` and a small capacity |
| 83 | +determined by how many items of `T` can be stored in two machine words. However, |
| 84 | +both of these values may be overridden. |
| 85 | + |
| 86 | +If you want to change the alignment, you can use either `SmallArrayListAligned` |
| 87 | +or `SmallArrayListAlignedSized`, passing in the desired alignment. |
| 88 | + |
| 89 | +Changing the small capacity can be done using either `SmallArrayListSized` or |
| 90 | +`SmallArrayListAlignedSized`. Using a small capacity larger than the default |
| 91 | +will increase the overall size of the `SmallArrayList`, but it allows for |
| 92 | +storing more items before allocating. |
| 93 | + |
| 94 | +For example, on a 64-bit system: |
| 95 | + |
| 96 | +```zig |
| 97 | +const std = @import("std"); |
| 98 | +const expect = std.testing.expect; |
| 99 | +
|
| 100 | +test "sizes" { |
| 101 | + const a = testing.allocator; |
| 102 | +
|
| 103 | + const List1 = SmallArrayList(i32); |
| 104 | + const List2 = SmallArrayListSized(i32, 6); |
| 105 | +
|
| 106 | + // This holds true on a 64-bit system |
| 107 | + try testing.expect(@sizeOf(List1) == 24); |
| 108 | + try testing.expect(@sizeOf(List2) == 32); |
| 109 | +
|
| 110 | + var list1: List1 = .empty; |
| 111 | + var list2: List2 = .empty; |
| 112 | +
|
| 113 | + defer list1.deinit(a); |
| 114 | + defer list2.deinit(a); // don't strictly need to deinit list2 |
| 115 | +
|
| 116 | + for (0..List2.smallCapacity) |i| { |
| 117 | + try list1.append(a, @intCast(i)); |
| 118 | + try list2.append(a, @intCast(i)); |
| 119 | + } |
| 120 | +
|
| 121 | + try testing.expect(list1.hasAllocation()); |
| 122 | + try testing.expect(!list2.hasAllocation()); |
| 123 | +} |
| 124 | +``` |
0 commit comments