Hacker News new | ask | show | jobs
by samsquire 1118 days ago
Thank you for this, this is helpful.

I wrote a JIT compiler and I didn't bother calling free much, I just let the operating system free up all allocated memory.

I got into this situation often:

   return_struct = do_something(mystruct);
   return_struct->inner_struct = malloc(sizeof(struct my_inner_struct));
Now, who owns inner_struct? Who is responsible for freeing it? Do I free it when I assign to it?

I feel this ownership complicates cross-language FFI API calls, because who is responsible for freeing structures depends on the application and the platform you're running under. For example, Rust code being called from Erlang. You have to be extra careful when memory is managed by a different language runtime.

I feel I am at the beginning of intuitively understanding what memory really is: memory is just a huge contiguous region of numbered locations. Bump allocators allocate in one direction and free all at once. Arena allocators allocate to a preallocated region, I think.

Memory is a logistical problem of how you arrange and allocate finite resources.

I am thinking of alternative visualizations of understanding memory, for example, I started writing an animation of binary search:

https://replit.com/@Chronological/ProgrammingRTS

The idea is that you see values and memory locations move around with the final goal being able to command memory to move around and be computed, such as real time strategy game.

I think if we could visualise memory as cars on a road, we would see obvious traffic jams.

5 comments

> Now, who owns inner_struct?

return_struct does since it is the only thing that knows the address.

> Who is responsible for freeing it?

return_struct is, unless you hand that responsibility over to something else.

> Do I free it when I assign to it?

Yes, unless you want leaks.

> I think if we could visualise memory as cars on a road, we would see obvious traffic jams.

That visualisation is helpful for threads, where the program is the road/map and the cars are the threads. I don't see how it's useful for memory.

A struct can't own something - it isn't a class with a destructor or anything. So it isn't quite so obvious. There are only two lines of code here, but the implication is a function is running that code and then returning `return_struct`, which might get passed around to more functions, and even returned further up the call stack. Somewhere there needs to be code that knows "hey - nobody else is using return_struct, and by the way you need to free return_struct->inner_struct before freeing return_struct.
The implementor of return_struct provides a destroy() function, and because you’re in C land, the programmer knows when to call it.
> I feel I am at the beginning of intuitively understanding what memory really is: memory is just a huge contiguous region of numbered locations.

There might be an analogy here that could help you reason about your nested structure allocations…

Memory is an array of bytes owned by the OS. While there are all kinds of implementation details about addressing and storage and performance and paging and virtual memory, it’s really just an array. The OS gives you a way to reserve pieces of the array for your own use, and you’re responsible for giving them back if you want to play nice and/or run for a long time, otherwise (as a safety net) the OS will take them back as soon as you exit.

This is, in a sense, very similar to the question you posed. An outer routine owns the outer structure, and an inner routine allocates some inner structure. The simplest, most intuitive, and generally best advice is that whoever allocates is also responsible for freeing memory. In other words, one way to define ownership of memory is by who allocates it. Implicitly and automatically the responsibility to free that memory belongs to owner that allocated it. It’s okay to explicitly transfer ownership, but can easily get complicated and unintuitive. You can also consider letting the parent free your struct to be similar to not calling free() in your JIT compiler - it’s a ‘lazy’ optimization to have the parent clean up - and I don’t mean that in a judgemental sense, I mean it’s valid to let the parent handle it, if you know that it will, and this can be done without getting confused about who owns the memory and who was actually responsible for freeing it. Note that when you leave the parent to clean it up, you are foregoing the ability to re-use the memory - this is true in your JIT compiler and it’s true for malloc() and free() as well. If you let the OS handle it, you’re in effect declaring that you believe you do not need to recycle the memory allocated in your program during it’s execution. (This might be true, and it might stay that way, but it’s always worth asking if it will remain true, since lots of people have been eventually bitten and had to retroactively refactor for memory management when their requirements change.)

Yeah, I hear you. I've not done a lot of FFI stuff directly, it scares me.

Arena allocators are cool, the idea is you allocate a large-ish region of memory and sub-allocate into it (often with a fast, simple allocator like a bump allocator) and then free the large-ish block when you're done. It's a way to take knowing how much memory you need as a whole and optimise that to a single call to malloc/free.

You may enjoy looking through https://www.cs.usfca.edu/~galles/visualization/Algorithms.ht....

Thanks for the link to the animations.

I want an extremely performant deep copy solution, I've been thinking of using an allocator to implement it.

If we have a tree data structure or a nested hashmap, then we want to copy it cheaply, there is copy on write. But most copies of hashmaps are slow because they instantiate every child object in a recursive loop.

So I want to be able to memcpy a complicated data structure for cheap copies.

> You have to be extra careful when memory is managed by a different language runtime.

While it would be nice to have next to no overhead for FFI, it's not always tractable. That's why you have to serialize across boundaries, the same as if you're serializing across processes or the network. At least in a single virtual memory space you can have a caller allocate a buffer and the callee fill it, with the caller being responsible for deserializing and freeing later. That gets you pretty far, and is relatively safe.

The alternative is to be callee managed, and for the callee to return things by handle and not necessarily by pointer, but that is also fraught.

That's exactly why you should use reference counting