Do you need to free memory after function returns?

Hey, I’m learning a Zig language and trying to understand the concept of manual memory management.

I have a function which uses ArrayList to accumulate some values.

ArrayList asks you to provide type of memory allocator you want to use.

When do I want to use page_allocator vs ArenaAllocator?

Also, when function returns, does it free memory automatically or you have to manually do it with e.g. defer std.heap.page_allocator.free(variable)?

If you need to do it manually and you don’t want to do it in that function, can you do it later somehow or memory will be buried in a runtime somewhere until program exits.

example

fn getList() []u16 {
  var list = std.ArrayList(u16).init(std.heap.page_allocator);
  list.append(1);
  list.append(2);

  return list.items;
}

Also, does it mean that code above will allocate two pages of memory? or 1 page will be used until it fills to the brim

In the case where my function is going to allocate something and then return that value, I give the allocator as an argument, much like the std library does. Then, the memory should be freed from wherever the function was called. In this case we would say that the caller owns the memory. If you were to put in the call to free the memory inside the function using defer, then your memory will be freed when the function exits and you will have a use after free bug.

It seems daunting at first, but you just have to stop and think about where and when that piece of memory is being used and when you are done with it.

The Arena allocator is kind of special in that you don’t have to free the individual bits of memory a piece at a time, at least to my understanding. Instead, the memory is freed in one go when it is deinitialised. This can be useful if you have a number of allocations in one module or function, as it decreases the housekeeping.

1 Like

Also, in the case of the ArrayList, I believe it allocates a block and just uses a bit more of it each time you add to it, only doing another allocation if there isn’t enough left in its internal block of memory.

1 Like

I think that is the idiomatic way to do it:

fn getList(allocator: *Allocator) ![]u16 {
  var list = std.ArrayList(u16).init(allocator);
  defer list.deinit();

  try list.append(1);
  try list.append(2);

  return list.toOwnedSlice();
}

pub fn main() !void {

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    var allocator = &gpa.allocator;

    var list = try getList(allocator);
    defer allocator.free(list);

    //Do something with your []u16 slice
}
5 Likes

Is the defer list.deinit() actually required here? I thought that toOwnedSlice() was sufficient on its own - I think docs say "Deinitialize with deinit or use toOwnedSlice". I’ve tried it both ways and neither seems to cause a memory leak:

const std = @import("std");
const expect = std.testing.expect;

fn getList(alloc: *std.mem.Allocator) ![]u16 {
    var list = std.ArrayList(u16).init(alloc);
    defer list.deinit(); // Is this required? Works with it commented out.

    // Put lots of items in or the memory leak may not show up
    var count = @as(u16, 1);
    while (count < 1000) : (count += 1)
        try list.append(count);
    return list.toOwnedSlice();
}

test "toOwnedSlice test" {
    const alloc = std.testing.allocator;
    var list = try getList(alloc);
    defer alloc.free(list);
    try expect(list[0] == 1);
    try expect(list[1] == 2);
}

Commenting out the defer list.deinit() seems to make no difference. Is there a good reason to include it?

In my case, I asked myself what benefit is there in not doing the deinit when it’s almost certainly a no-op given the context. Performance? I suspect any performance difference is negligible. But the benefit of leaving it in can be significant if for some reason in the future the internal implementation of the structure in question (ArrayList in this case) changes, making the deinit call required or just an added benefit; you would be covered automatically, and not have to go back and add the call all over your code.

1 Like

I think it is a good practice due two reasons:

  • Your code can fail and return before the return list.toOwnedSlice();, so you need a defer.
    With defer, you can introduce any changes in the future, without worry about leaks.

  • Without looking at toOwnedSlice implementation, you can assume the list potentially allocates more memory than actually is used (capacity > length); So you don’t have to rely on any specific List implementation.

2 Likes

Two things:

  • It doesn’t hurt, because std.mem.Allocator.free() that’s internally used by std.ArrayList(T).deinit() doesn’t do anything when the memory to free is already empty (0-bytes-sized).
  • I would leave it there so that it can clean up the memory owned by the std.ArrayList(T) in case one of the appends in the loop fails. Pedantically, one could make it errdefer and do nothing on normal returns, but this is also fine.

this is a great resource for learning about allocators

1 Like