Initializing Stack Buffers: std.mem.zeroes vs. undefined

Hey everyone,

I’ve been thinking about how I initialise stack-allocated buffers in Zig, and I’ve found myself consistently leaning towards std.mem.zeroes rather than undefined.

For example, I’ll typically write:

var buf = std.mem.zeroes([256]u8);

Instead of:

var buf: [256]u8 = undefined;

My reasoning is primarily around robustness and clarity. It feels “safer” to me to explicitly zero out a buffer, especially when parts of it might not be immediately overwritten, avoiding potential issues from reading “rubbish data.” It also seems to make debugging and understanding the code’s initial state a bit clearer.

However, I understand Zig’s philosophy often leans towards avoiding unnecessary work and “pay for what you use.” Initialising with zeroes does incur a write cost for the entire buffer, which undefined avoids.

So, I’m curious about the community’s perspective on this, with a focus on robust, readable, and understandable Zig code:

  1. Is there a significant downside to my preferred std.mem.zeroes pattern that I might be overlooking, beyond the potential minor performance overhead for large buffers?
  2. In what specific scenarios do you strongly advocate for undefined when writing robust Zig code?
  3. Do you have a general rule of thumb for when to use one over the other, keeping readability and robustness in mind?

I’m keen to hear your thoughts and best practices!

(Full disclosure: I used an AI assistant to help me structure and word this post, as I sometimes struggle to articulate my thoughts clearly. All the underlying questions and reasoning are my own.)

If you are compiling your code in debug mode Zig will set undefined memory to 0xAA anyways which is arguably easier to spot in a debugger than zeroes since there’s no chance that it’s just a fresh page from the OS.

Lots of C code is built around using 0 as a default initialization value because it’s convenient to just memset an entire struct to 0 to initialize it. Also global/static variables automatically get zero-initialized in C. And there’s even a special syntax for arrays (int array[256] = {0}, this only works with 0 as a special value).
IIRC translate-c also sets the default values of all C types to std.mem.zeroes(...).

This is not the case for Zig, containers can have explicit default values, globals have to have an initial value and string usually aren’t zero-terminated, so there’s really no reason to carry over zero initialization from C IMO.

7 Likes

Using splat or zeroes for large arrays can bloat your binaries. I’d be careful with them.

1 Like
  1. You would lose out on Valgrind’s ability to detect the use of uninitialized memory. See What's undefined in Zig? - Zig NEWS (specifically the section What does undefined do?). Additionally, if debug safety feature: runtime undefined value detection · Issue #211 · ziglang/zig · GitHub ever gets merged you’d be SOL, but it doesn’t look like it’s gonna be any time soon.

  2. For data that are going to get modified later on, almost always. Intent is more clear, IMHO. It’s also shorter and easier for me to type :winking_face_with_tongue:

  3. I try to use undefined as the language reference prescribes: Use undefined to leave variables uninitialized.

To bikeshed for a moment, initializing a buffer with all zeroes and leaving a buffer uninitialized are two distinct operations, both with their own merits (though, historically, and especially in other languages like C, initializing a value with all zeroes can be seen as leaving it uninitialized).

At the end of the day, I think it’s really about personal preference, and how closely that preference aligns with “idiomatic Zig.”

2 Likes

Zeroing an array just for the sake of it js the opposite of robustness and clarity.
The reader has to look ahead in the code to understand if 0 is a meaningful value.
In a debugger, a zeroed array is much harder to distinguish from the background.
0 is also very frequently a valid number. If the number you’re initialing is an index into an array, 0 will always be valid. If your intent was to initialize that variable to some other number, zeroing it may hide a bug. undefined in debug builds sets it to a value purposefully chosen to crash in a large number of scenarios, like indexing into array, which is much better than hiding a bug.
The compiler isn’t always able to optimize the zeroing away, so you’re also just wasting cycles.

7 Likes

Yeah i agree totally. Zeroing is less clear than undefined and it is a waste of our poor cpu who is already so busy.

I only fill mem when I need an “empty” version of a (not too big) struct like
pub const empty: Self = .{}

Actually I like undefined very much. Just don’t access the undefined mess :slight_smile:

2 Likes

Zeroing memory can mask latent bugs in your program.

When dealing with a buffer, you will generally be doing potentially partial filling, and that operation should always return how much it filled, which you then use to access only filled data (often by taking a slice which refers to only the valid data or having a length field in the same structure containing the buffer).

If you need zero termination, then write just the terminator – one unit and not hitting the entire span of memory, which will cause pointless cache invalidation for larger buffers.

4 Likes

undefined in debug builds sets it to a value purposefully chosen to crash in a large number of scenarios, like indexing into array, which is much better than hiding a bug.

My understanding is that this only happens when the index into the array is comptime-known. Otherwise, you’re indexing the array with undefined memory (i.e. 0xAA...) which would exceed the bounds of the array. It crashes in both scenarios, but it’s not always clear why it crashed in the latter.

You are correct, but if the memory comes from std.mem.Allocator for example, it sets it to undefined

That’s the point. It will crash. Contrast that to initializing to 0, which will surreptitiously work.

2 Likes

Thanks everyone for your input. This has been illuminating.

I think that I had long held assumptions that don’t really apply to Zig. So it’s great that there is such a nice community here that I and others can ask these sort of questions and not get lambasted for it.

10 Likes

It’s worth noting that std.mem.zeroes() doesn’t work with unions (including error unions).

Yeah, I found many bugs in my code thanks to garbage values in my freshly allocated buffers.