BoundedArray vs open-coding

I often need to process a constant amount of things in a single function. It feels that what I should be doing is using a bounded array:

var items: std.BoundedArray(Item, 16) = .{};

However, I find BoundedArray API to be quite verbose, and often end up open coding it on the spot:

var items: [16]Item = undefined;
var item_count: usize = 0;

Which is considered more idiomatic Zig? How can I to stop avoiding and love the BoundedArray?

4 Likes

Don’t you think that making an ad-hoc BoundedArray results in even more verbosity? Certain things that would be an API call turn into multiple lines.
Also, using the same BoundedArray in multiple functions leads to code reuse, with smaller binaries and better cache locality.
Slightly tangential, but I remember seeing in a PR a comment from Andrew saying that the Zig compiler stopped using BoundedArray in favor of ArrayList, because it resulted in better code generation. Anyone could elaborate on this?

I used to have a lot of small arrays with counters as well, but nowadays I’ve almost exclusively switched to allocation and array lists. Of course you need a good (and thread local) allocator for this, but the big advantage for me is that I don’t have to worry about setting the upper limit right.

As for code generation of BoundedArray, Zig has some trouble with arrays in structs, and in certain situations it will just copy the entire array when accessing a single element. Here is the relevant issue: Another array access performance issue. · Issue #13938 · ziglang/zig · GitHub

1 Like

When you say “constant”, I hear known lower and upper bound which are the same number. If that’s correct, I would use an array personally, there seems to be no downside.

BoundedArray is more for a known upper bound, but where the actual number of elements is runtime-determined. I’ve never had occasion to use one actually, it seems like the idea is that it can use the familiar ArrayList methods but with a stack allocation of constant size?

I’m sure that can be useful, but if I need to store exactly sixteen of something I use a plain-old-array and a separate index. Might be force of habit from other languages but it works.

If you like the ArrayList API better and know you will not add more items than the bounds of the buffer allows, then you can do:

var items: [16]Item = undefined;
var list: std.ArrayListUnmanaged(Item) = .initBuffer(&items);

// ...
// note: do *not* call any ArrayListUnmanaged functions that take an allocator

The original intention with initBuffer was to replace BoundedArray entirely (the PR that added it originally had the title std: remove BoundedArray), but BoundedArray was ultimately kept because it can be used for different use cases (e.g. it allows safe copying).

One thing that would make using initBuffer nicer would be std.array_list: Add *NoResize functions to ArrayListUnmanaged by notcancername · Pull Request #18361 · ziglang/zig · GitHub, which is on my list of PRs to revive at some point.

8 Likes

Here is an example of a BoundedArray that I wrote:

var result: std.BoundedArray(u8, 2048) = .{};
const writer = result.writer();

In order to replace this with an ArrayListUnmanaged, I came up with this:

var result_buf: [2048]u8 = undefined;
var result: std.ArrayListUnmanaged(u8) = .initBuffer(&result_buf);
var fba: std.heap.FixedBufferAllocator = .init(&result_buf);
const allocator = fba.allocator();
const writer = result.writer(allocator);

This is a bit more verbose. Should I expect the generated code to be better? In the PR you linked, Andrew said this:

I am tempted to remove the API entirely since it in practice leads to less than ideal code, […]

Why does the BoundedArray API lead to unideal code? Isn’t it just a buffer and a runtime-known length?

You can do this without the extra steps using a BufferedWriter.

I personally have need for this type of idea quite a lot in my golang projects, when parsing headers for specific protocols that define a maximum header length (like the Gemini Protocol). I haven’t had a chance to use them in Zig yet, but it seems like it could serve that purpose very well.

Although, now I’m concerned about the performance issues mentioned above.

2 Likes

I’ve had the thought to add similar functions myself. I would nitpick the names though, I think appendNoResize collides with the resize function in an unfortunate way. I think a better name might be appendCheckCapacity, or appendNoAlloc.

Are you thinking of std.io.fixedBufferStream? std.io.BufferedWriter expects to wrap another Writer, where in this case they have a fixed size buffer they want to write into.

1 Like

I think tryAppend is better, but that might be confusing since append already returns an error union

I absolutely was, thanks for catching that. You can see where the names might be confusing I think.