January 7, 2026, 5:29pm 1
This may have been discussed before, and if so feel free to point me to that discussion. I couldn’t find it though.
I’ve been playing with optionals for value types (i.e. not pointers), and I’m I little surprised that the implementation seems a bit basic. If I have a ?u32, that looks something like this (Not literally, but from a memory layout point of view):
const OptionalU32 = struct {
valid : bool,
value : u32,
};
The size of it is 8-bytes as it’s aligned to 4-bytes. This is understandable. For a single instance of this, there aren’t many options. However, If I have an array of 4 of them, that array is 32-bytes in size. It could apply the “structure of arrays” pattern and produce this (again, from a memory layout view):
cons...
January 7, 2026, 5:29pm 1
This may have been discussed before, and if so feel free to point me to that discussion. I couldn’t find it though.
I’ve been playing with optionals for value types (i.e. not pointers), and I’m I little surprised that the implementation seems a bit basic. If I have a ?u32, that looks something like this (Not literally, but from a memory layout point of view):
const OptionalU32 = struct {
valid : bool,
value : u32,
};
The size of it is 8-bytes as it’s aligned to 4-bytes. This is understandable. For a single instance of this, there aren’t many options. However, If I have an array of 4 of them, that array is 32-bytes in size. It could apply the “structure of arrays” pattern and produce this (again, from a memory layout view):
const OptionalU32x4 = struct {
valid : [4]bool,
value : [4]u32,
};
That would be 17-bytes (all the valid bits packed into a byte) or 20-bytes (one byte per “valid” flag". Both would be far less wasteful.
Does this optimization not work out for some reason? or is it just not common enough to worry about at the moment?
Also, I’m surprised that if I reduce the type size to a ?u31, the compiler doesn’t use the (now unused) MSB of the 32-bit location, effectively making it a packed struct and compressing things that way.
Edit: Observations on 0.15.2 if it matters.
1 Like
Sze January 7, 2026, 5:45pm 2
WeeBull January 7, 2026, 5:55pm 3
Thanks… that’s the “using the unused bits of a ranged int” type of approach. Following the discussion a bit more I found allow integer types to be any range · Issue #3806 · ziglang/zig · GitHub, which seems to be a significant discussion about that concept (and more).
I was more focused on the SoA type approach though.
Sze January 7, 2026, 6:13pm 4
If you want to use SoA, I guess you could use std.MultiArrayList(struct { bool, u32 }) or use a std.bit_set.DynamicBitSetUnmanaged alongside a std.ArrayList(u32).
If you can move your instances you also could have two Archetypes/groups so you would have two std.ArrayList(u32) where one contains the ones which are true and the other the ones which are false.
For what you have described, small SoA batches, that certainly is possible too, but I am not aware of anything automatic or half-automatic that helps with that, I think you would have to write it manually or do some meta-programming.
I think it would also be thinkable to create a variant of MultiArrayList that either does the sort of batching you described or uses BitArrays for small types. But I think that would require changing the API, because it uses some slice based accesses that wouldn’t really work well with BitArrays or Interleaved data. So maybe it could expose an iterator instead.
1 Like
mnemnion January 7, 2026, 7:12pm 5
The general term for this kind of thing is ‘niche optimization’. Currently Zig has one: a ?* has the same size as a *, and will use NULL to represent null.
Expanding this to cover more cases is planned. Implementing it manually is reasonable, I’ve done it myself once. More often I just use a ?u31, if the semantics make sense for the application: the idea that one day I’ll download Zig and my code will get more efficient is satisfying, in its own way.
3 Likes
dimdin January 7, 2026, 7:46pm 6
For optional encoding you need only a spare value.
A syntax like enum (u32) { _, null = 0xffffffff } make sense for declaring the specific value used for null.
With one bit you can get both optional and errors.
I need it for a packed struct(u127) { ... }; to encode both optional and error values.
Zig error values start at 1 and by default end at 65535. 0 can be used to encode null.
The spare bit can be used as a tag for the sum type to distinguish between the actual packed struct data and the null or error code.
I think we will get there eventually.
2 Likes
mnemnion January 7, 2026, 9:09pm 7
Yes, the spare value is called a ‘niche’. It’s where you put null.
In a language with niche optimization, if a type has an invalid or arbitrary bit sequence within its size, that will be used to represent null for the type.
Invalid: a ?u31 (not packed), half of all bit sequences which fit in four bytes are invalid, one of them is used, doesn’t matter which.
Arbitrary: If a struct type has any padding, some pattern within the padding is used. Whether we want to think of the high bit in a four-byte u31 as padding, or a component of invalid numbers four-byte integers which don’t fit, is dealer’s choice I suppose.
My impression is that adding the optimization (which is obviously good) has been sorted behind integer ranges (as @WeeBull suggests), the relationship between those things being fairly clear. That would allow for optionals like ?@Int(0, 0xffff_fffe) as well as ?@Int(1, 0xffff_ffff), one of which uses the maximum u32, and the other the minimum[1].
Probably if someone just wanted to tackle it for the existing odd-width integer types, we’d have it faster. There aren’t all that many people working on the language, when you come right down to it. And it’s ‘just an optimization’, it makes things more efficient, it’s nice, but it’s not hard for me to see what other things are considered higher priority.
Closed intervals! It’s important!! ↩︎
1 Like
const a: [4]?u32 = undefined;
const b: [*]const ?u32 = &a;
Because pointers to arrays can be implicitly converted to many-item pointer. I think this layout would break that.
vulpesx January 8, 2026, 3:37am 9
I think you are thinking of converting their SOA to your AOS. Ofc you can’t do that, they are fundamentally different data layouts.
But you can just keep the SOA layout with ptrs instead.
struct {
valid: [*]bool,
value: [*]u32,
}
You can even add a len to get an SOA slice.
1 Like
Yep, that’s the point. My understanding is that OP wants the array type to automatically optimize AOS to SOA, but for Zig, the memory layout and mental model of arrays are stable, and there is a fundamental difference between AOS and SOA.
vulpesx January 8, 2026, 3:53am 11
Then I fail to see your point, you would just design your APIs to use whatever you chose to use.
I doubt there are any useful std APIs for either, in this specific case.
In fact, I think SOA allowing you to get a slice of just the tag or the value makes more std APIs useful, though still not much. But it is certainly quite useful for your own APIs.
Alright, I don’t really have an opinion; I was just explaining to the OP why the memory layout of AOS wouldn’t be optimized into SOA. At first, the example I used of an array pointer implicitly converting to a many-item pointer might have been a bit awkward, which caused some misunderstandings.
1 Like
vulpesx January 8, 2026, 4:02am 13
Ah, I got distracted by all the niche optimisation talk, I missed that op was speculating if zig would do an SOA transformation as an optimisation.
The answer is no as zig gives you full control over your data, doing an implicit SOA transformation would cause a lot of issues if you were unaware of it. And zig is incapable of always knowing when such a transformation is good.
1 Like