Is deinit necessary when using ArenaAllocator?

Hello there, I’m new to Zig, have a lot of questions. Here is one of them:

I’ve written a simple program to download files and it works. But then I started experimenting. According to the Zig docs I replaced GeneralPurposeAllocator with ArenaAllocator, which means there is no more need to free manually, so I omitted deinit of objects. After that I even disabled file.close, argsFree and arena.deinit, but the code still compiles and works. I wonder why - shouldn’t this have led to memory leaks?

Here is the program (fetch.zig):

const std = @import("std");
const fs = std.fs;
const print = std.debug.print;


const State = struct {
    allocator: std.mem.Allocator,

    fn fetch(self: State, url: []const u8) !void {
        const uri = std.Uri.parse(url) catch unreachable;
        const name = fs.path.basename(url);

        var client = std.http.Client{ .allocator = self.allocator };
        // defer client.deinit();

        var server_header_buffer: [1024]u8 = undefined;

        var req = try client.open(
            .GET,
            uri,
            .{ .server_header_buffer = &server_header_buffer }
        );
        // defer req.deinit();

        try req.send();
        try req.finish();
        try req.wait();

        if (req.response.status != .ok) {
            return error.HttpFailed;
        }

        const cwd = fs.cwd();
        const file = try cwd.createFile(name, .{});
        // defer file.close();

        var buffer: [1 << 14]u8 = undefined;

        var i: u64 = undefined;
        print("Fetching data\n", .{});
        while (true) {
            i = try req.read(&buffer);
            if (i == 0) break;
            _ = try file.write(buffer[0..i]);
        }
        print("Successful download\n", .{});
    }

    fn parseArgs(self: State) !void {
        const args = try std.process.argsAlloc(self.allocator);

        if (args.len == 2) {
            try self.fetch(args[1]);
        } else
            print("Usage: fetch [URI]\n", .{});   
    }
};

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    // defer arena.deinit();
    
    const state = State {
        .allocator = arena.allocator()
    };
    try state.parseArgs();
}

It does leak all those things you listed.

But those leaks do not become an immediate problem. As long as you do not hit operating system limits like maximum count of open files or available system memory.

3 Likes

Thank you for response. As I far as I understand, to be aware of possible leaks I should use GPA instead of ArenaAlloc. during developing, right?

I feel like this is a misunderstanding of what leaks are, when they matter, and how they are detected.

As mentioned, you are ‘leaking’ the memory you’re allocating, but:

  • No matter what, the operating system will reclaim that memory when the process exits (i.e. the program cannot hold onto memory after it finishes)
  • std.heap.GeneralPurposeAllocator (now renamed to std.heap.DebugAllocator) does extra work to detect and report leaks, so now that you’re no longer using it, your program won’t report leaks
    • If you instead changed your arena to be backed by a DebugAllocator instead of page_allocator, then the lack of arena.deinit() would cause leaks to be reported
    • If you used another leak checker like valgrind, it would also detect the leaks

So, if your program can run fine with no freeing of any allocated memory, then ‘leaking’ all the memory is not necessarily a problem. However, leaking becomes a problem when your memory usage can grow unbounded during the course of the program. Here’s an example:

while(true) {
    var bytes = try allocator.alloc(u8, 12345);
    // defer allocator.free(bytes);
    // ...
}

With the defer free commented out, each loop iteration will allocate bytes that will stay mapped forever, so the longer the program runs, the more memory it will use (think of a long-running program like a game, etc). This is a contrived example, but the point I’m trying to make is that leak checking is useful to ensure that you don’t accidentally introduce this sort of problem throughout your program/library.

Ultimately, leak checking (and fixing leaks) just makes sure that your program/library won’t run into the problems caused by leaks (see here for an extra wrinkle on the testing side of things). Leaking itself is not necessarily a problem on its own, though.

(FWIW, I personally never intentionally leak resources in the Zig code I write)

7 Likes

(post deleted by author)

Thanks a lot! Now it’s fully clear to me.

1 Like