Who frees up memory when errdefer is used with allocator

Hi all. New to Zig and also to languages that require the developer to manually manage memory, so pardon my “noob-ness”.

Hence this post is partly a question and partly asking for confirmation on some assumptions I am making.

So I just finished going through the allocators section of ZigLearn.

The first question I asked myself, after reading that section was, when do one even need to manually allocate memory?

This led me to this StackOverflow answer here where the author mentioned that:

You use malloc when you need to allocate objects that must exist beyond the lifetime of execution of the current block (where a copy-on-return would be expensive as well)…

I think a function is a perfect example of a current executing block, this then led me to wonder, if the above is indeed the case, then how useful is the pattern of allocate/free with defer, I just read about?

I ask that because I was thinking of a situation where a function allocates memory and returns it. But if the memory is freed when the memory exits, then that prevents the allocated objects from existing beyond the lifetime of the executing block right?

Then I found the post here How to return an ArrayList after allocate in function? where this very question was asked. Reading the responses I found the way to go is to use errdefer instead of defer.

Okay that does sounds like I have the answer to my question. Just that later in the thread it was mentioned that:

It’s generally a code smell to return an ArrayList as the return type of the function. It tends to work out better to accept a mutable ArrayList as a parameter, and have the function append to it.

Okay, I get the rationale for saying returning an ArrayList is a code smell, but I am now wondering, what if you have found a valid scenario where returning an allocated memory is indeed what you have to do, who then makes sure that the memory handed out by the function actually get freed?

The other thought/question is regarding best practices. Will I be right to say that the pattern then is to always allocate all the memory the program will need in main and then pass down mutable reference to this memory? Then at the end of main, a defer ensures it is cleaned up?

Or there are other ways/patterns to go about this?

1 Like

Welcome to Zig!

when do one even need to manually allocate memory?

There is actually a few more reasons, apart from wanting to have something outside the function.

  • Sometimes you don’t know at compile-time how big something is, like when reading a file, manipulating strings, collecting things in an arraylist.
  • Sometimes you need too much memory for the stack. Let’s say you have a 100MB array, but the stack is only ~8-16 MB.

And at least from my experience these are fairly common. I would guess that majority of my use-cases fall into the first category.

But if the memory is freed when the memory exits, then that prevents the allocated objects from existing beyond the lifetime of the executing block right?

Exactly. That’s why you shouldn’t use defer free when you want to return it. errdefer free is used to prevent resource leaks in case of an error in the other code.

what if you have found a valid scenario where returning an allocated memory is indeed what you have to do, who then makes sure that the memory handed out by the function actually get freed?

The person who calls the function must do that (which is probably you). So you should make sure that the caller knows this (often when you pass an allocator to a function it means that you should clean up the result)
However don’t worry too much about messing up. If you used the GeneralPurposeAllocator, it will catch your leak and tell you where the memory was allocated. This is really helpful and makes finding leaks super easy.

Will I be right to say that the pattern then is to always allocate all the memory the program will need in main and then pass down mutable reference to this memory? Then at the end of main, a defer ensures it is cleaned up?

Not quite. It would be really cumbersome(or even impossible) to allocate all memory in main.

Generally I try to allocate in the deepest function such that I can still free it in that function. If I don’t need something outside the current function, then it should be allocated and freed inside of it.

And secondly it often makes sense to abstract some heap allocated data into a struct (ArrayList is an example of that). Here the convention is that the deinit function frees all the data that was allocated during the initialization and life-time of the object.
So better make sure to call defer list.deinit();

And as soon as things need to be shared between threads these best practices are no longer applicable. Then I think you just need to be careful with what you do, but multi-threading is a whole other topic.

I would encourage you to just start programming, then you’ll figure out when you need to allocate dynamycally and what patterns work for you. If you use the GeneralPurposeAllocator, it should also be easy to find and debug memory issues you may encounter.

3 Likes

Thanks for the response. I just have one bit, I need your clarification on. When you said:

But how is that possible when I only call the function, and I get a chunk of memory back? Or does it mean I need to define such a function to take in an allocator? (instead of using one it creates within its own function body) - and then use the allocator passed into the function, which was used to allocate the memory to free it?

Or is it possible to just free a chunk of memory without needing to pass in an allocator?

Yes, you need pass in an allocator (or use a global one).
Otherwise, when you create one in the function(don’t forget defer gpa/arena/fba.deinit()) you are clearing the allocator at the end of the function, which usually also frees the underlying memory as well.

2 Likes

It might be helpful to see an example from the standard library:

(you can ignore the loop, just look at the var result = ..., errdefer, and return result.toOwnedSlice() lines)

And its usage within a test case:

You also might be interested in this blog post I wrote if you want to go down the rabbithole of errdefer and allocation errors: An Intro to Zig's checkAllAllocationFailures - ryanliptak.com

2 Likes

So tried this without passing in an allocator

fn leakMemory() ![]u8 {
    const allocator = std.heap.page_allocator;
    const memory = try allocator.alloc(u8, 100);
    return memory;
}

pub fn main() !void {
    _ = try leakMemory();
}

So here I am using an allocator created within a function, return the memory it allocates and no defer anywhere to free it up.

This compiles fine.

Will I be correct to say that there is no compiler check to prevent these kind of issues? (somehow I was expecting to get a compiler error or at least a warning that I am not freeing up memory)

There’s no compiler check however the std.heap.GeneralPurposeAllocator would catch this leak at runtime by calling .deinit() on the allocator at the end of it’s use.

1 Like

To give a concrete example of that:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.assert(gpa.deinit() == .ok);
    const allocator = gpa.allocator();

    const leak = try allocator.alloc(u8, 100);
    _ = leak;
}
$ zig run test.zig
error(gpa): memory address 0x7f7464303000 leaked: 
/home/ryan/Programming/zig/tmp/test.zig:8:37: 0x220f85 in main (test)
    const leak = try allocator.alloc(u8, 100);
                                    ^

thread 99886 panic: reached unreachable code
/home/ryan/Programming/zig/zig/lib/std/debug.zig:341:14: 0x221322 in assert (test)
    if (!ok) unreachable; // assertion failure
             ^
/home/ryan/Programming/zig/tmp/test.zig:5:27: 0x220fef in main (test)
    defer std.debug.assert(gpa.deinit() == .ok);
                          ^
2 Likes

Thanks for this example. Just to be sure I get the gist. The runtime panic is due to this line
defer std.debug.assert(gpa.deinit() == .ok);? And nothing inherently done by the gpa.

Because I did remove that line and the code ran without panic.

Meaning that the burden falls totally on the developer to make sure to insert these checks, and the compiler at the moment does not automatically check or nudge via warnings?

Gotcha.

I guess this is where knowing about the various available allocators, and their capabilities etc come into play?

And just to be 100% sure. In the code snippet I shared above. There is nothing the caller of leakMemory can do to free the memory it got? That memory is forever leaked and I guess falls on the OS to reclaim it when the process exits? Will that be correct?

If you lose the slice/pointer then it depends on the allocator used if it can be reclaimed before program exit. In this case it will live until the end of the program thus counted as a leak.

Do note that passing the allocator from the parent as a parameter allows for other strategies such as using an arena allocator which cleans everything up on .deinit().

Looking up arena allocator now! Thanks!

As I understand it, a leak is only relative to your program, not to the running host OS. Once any program exits, all memory allocated to it will be reclaimed by the OS. This behavior can be used as a valid memory management strategy. Don’t free any memory at all, and let the OS reclaim everything on exit. If I remember correctly, the D programming language compiler famously touted this as their memory management strategy for compiling programs, since it’s always a short-lived task. So as always, it all depends on context.

3 Likes

Correct, and it’s pretty unlikely that the compiler will ever be able to check for that sort of thing.

1 Like

defer std.debug.assert(gpa.deinit() == .ok);
won’t this assert get removed in a release build, and with it the gpa.deinit() call too?

In a release build, GPA won’t check for leaks anyway, so the assertion not being active won’t matter.

But the deinit call won’t be removed in a release build–that’s not how std.debug.assert works in Zig. See `std.debug.assert` causes evaluation of expression in release modes · Issue #10942 · ziglang/zig · GitHub

1 Like

Cool, thanks. Intreastingly, the code snippet povided in the issue is the same one you provided here too(the utf one) :smiley:

Nvm, wasn’t looking at the people’s names :stuck_out_tongue:

1 Like