I really like the GeneralPurposeAllocator’s ability to catch memory leaks and in my code I’ve asserted that my application shuts down with zero leaks detected.
This is great.
But, I did notice that my app which is a game happens to leak memory on one of my levels.
The reason I didn’t initially catch it is because I’m using SDL2. When I shutdown SDL it will gracefully cleans up leftover allocated textures/surfaces or memory left over. (At least this is what I think is happening)
So this got me thinking: Perhaps there is room for enhancement of the GPA where we can take baseline snapshots of the memory allocation count before loading a level and then call .deinitSnapshot after a level is unloaded to get an intermediate stack trace of what is leaking.
Details of how it could work: you use one global GPA for your app. Your initial app loads, you’ve possibly allocated 30 items on the heap. Next you do a GPA.snapshot() and then load a level. Your level loads 120 heap objects. Next you .deinit your level and do a GPA.snaopshotDeinit() and this will dump a stack of leaks if the value is larger than 30. If it’s just 30 at this point no leaks are reported until finally you shutdown your app and finally call: GPA.deinit() and now you want to see no report generated because your code is solid. What I’m describing is fundamentally a stack of allocations for a given scope.
I think the GPA reporting is an all or nothing approach right now but having a scoped GPA ability would be super cool to assert that certain lifetimes of an app are also handled correctly.
I suppose I could use a different GPA instance for my level loads. But having a single allocator that my app uses is also something I’d like to maintain.
Another thought I had: could I somehow just nest GPA allocators where one becomes the child of another to somehow do this scoping trick I’m interested in. I’m also aware of using an Arena for instance but I’m specifically interested in the memory leak stack trace and dumping it out at with different scopes of my application lifetimes.
If you suspect that the memory leak comes from SDL 2, then I don’t think there is any way for the GPA to catch this, since SDL 2, being a C library, uses the C allocator. You could maybe try using valgrind to debug the leaks from SDL.
I think this is the way to go.
There is no advantage of limiting yourself to using a single allocator in your entire program.
Using two allocators like this might even be helpful for the design of your program, since it makes it easier to distinguish between what should be a per-level allocation and what should be a persistent allocation.
I suppose one advantage of a single allocator is not having to create and maintain more allocators but I think at least in my case having an allocator that is responsible for my levels allocation should work too.
It’s funny you mention SDL2 possibly leaking. I went down that path investigating if SDL2 was leaking by using their API to set custom memory functions. If you provide your own malloc, calloc, realloc and free functions, you can tell SDL to use those for all memory.
I did this, providing my own rudimentary functions that just wrapped the GPA and I now I can also detect any leaks in all of SDL. I found none so far.
Oh interesting it looks like I can nest GPAs, I guess that’s still two different allocators but maybe this is what I can use anytime I want to track a scope of allocations. Thank you for some reason I didn’t think I could provide a child to the GPA (only the others).
Hello, can you elaborate further with an example please?
“you can use GPA.init(.{.backing_allocator = parent_allocator }) to create your child gpa instance.”
Because I am also working on the problem
I adopted the principle of vase management (module) and associated an arena for each vase, but sometimes I have the impression that there is a footprint left, I do not know if it is the system that is wrong (my reading software “gnome-system-monitor”) or me who is not doing what is necessary.
Let me explain, I have a main module that calls various modules as subroutines handling their problems in their entirety.
What I noticed is that once the module is loaded, the footprint remains even if all the variables are deallocated, even if I’ve exited the module and returned to the main for another story.
This is a problem I don’t have on mainframes.
const std = @import("std");
const expect = std.testing.expect;
test "GPA" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer {
const deinit_status = gpa.deinit();
//fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) expect(false) catch @panic("TEST FAIL");
}
const bytes = try allocator.alloc(u8, 100);
defer allocator.free(bytes);
}
The above code just demonstrates how the GPA can do leak detection when .deinit() is invoked.
In the code below however this is how you can setup a different child allocator instead of the standard one, where the standard allocator used by the GPA is the page allocator.
Here is an example of changing it:
var gpa = std.heap.GeneralPurposeAllocator(.{}){
.backing_allocator = <some-other-child-allocator>,
};
And you technically should be able to swap in any allocator that adheres to the std.mem.Allocator interface.
In my case, I was referring to using another GPA as a child which seems superfluous but the benefit is that your root GPA detects leaks at the end of the application while a nested/child GPA can be used on for example per level load/unload or per module load/unload as you have mentioned.