FTA: Then there is also calloc(), which allocates memory that has been zeroed. For whatever reason, it has a slightly different interface:
void* calloc(size_t num, size_t size);
Its interface is different because, historically, it was (more or less; prototypes didn’t exist yet and I’m not sure they used unsigned, for example):
void* alloc(unsigned int num);
void* calloc(unsigned int num, unsigned int size);
and ‘int’ was 16-bits, but pointers 32 bits. So, alloc could only allocate up to 65,535 bytes, calloc up to 65,535 items of size up to 65,535. So, it allowed you to allocate much larger arrays.
I also wondered quite a bit about this difference. One interesting advantage of this alternate API is that calloc can always check for overflows, since a common pattern might be “calloc(user_input, sizeof(type))”, where using the older API without an overflow check on the multiplication would be perfect for constructing a buffer overflow.
With respect to your explanation, I imagine there are some cases (maybe something older?), but in my experience the norm for an int on a system with 32-bit pointers is also 32-bits.
8086 has 16-bit registers and 16-bit near pointers but there are also 32-bit far pointers using the segment register.
PDP-11, where calloc most likely originated, had 16-bit integers and 16-bit pointers. Apparently, later PDP-11s could be split into 64k of code space and 64k of data space (like the Harvard architecture) but that would still use 16-bit pointers.
This article gets a lot right about the shortcomings of the standard malloc API. But I find it unfortunate that it starts off citing modern allocators including jemalloc, yet makes no mention that jemalloc has for nearly a decade provided the non-standard *allocx API to address essentially all of the discussed shortcomings. I encourage people to read the jemalloc manual page for comprehension.
malloc() and free() are a far better API than one that requires unwrapping and casting pointers every time you use them, and - despite trying to keep metadata with the pointer - still requires you to separately track metadata!
Also, throwing C++ code in when he's showing C is wrong on so many levels.
(I just remembered a similar discussion by Richard Heathfield in C Unleashed. I looked it up and it's on pages 285-286. He sets out to improve upon the design of realloc(). He concludes "that whoever designed realloc did know what they were doing, after all. There's a lesson there somewhere for overzealous wheel inventors.")
The proposed ideas are... fine? I don't know that I would use them, for reasons others have pointed out (I don't wanna have to cast pointers constantly if I don't have to). I am not the most seasoned C/C++ programmer, but I've done my share, and I don't think the use case described here regarding page boundaries has ever come up. Maybe someone else can comment on how common it is when you spend 100% of your time with these languages.
Fundamentally, though, I don't think that "malloc() and free() are missing some features that might improve them for my use case" is the same as "malloc() and free() are bad APIs." They're okay APIs, and they're like anything. Once you know what to expect, you barely think about it anymore.
I do think it would be interesting to hear more about the logic that inspired the original C developers to design the APIs as they did.
When he says they are bad APIs, I think he means they aren’t a proper basis API for interacting with a memory resource. You can implement malloc and free and realloc and callow in terms of his API with no overhead but not the other way around.
The use cases he cites are corner cases, as others have argued.
In my mind, a far more common use case calls for memory pools, where memory can be allocated and not explicitly tracked by the application. The memory pool gets freed at the end of some unit of computation. This interface is already nicely supplied by an Apache memory pool library.
Don't forget that they don't allow custom allocators. Arena allocators can be far more efficient, but aren't supported by any of the standard data structures.
The idea that if you provide an alternative to free which requires the originally allocated alignment and size then users of it will provide the correct values is very naive. Rust's allocator interface looks very similar to malloc and free. I can guess which functions the main implementation of that calls.
I think a big flaw with malloc is that the returned values are memory addresses and not just handles. That means objects can't be moved around in memory once allocated. There's also no notion of types with malloc, just memory sizes in bytes. That makes heap fragmentation more of a problem. Plus if you have an allocated object containing a pointer to another allocated object there's no easy way to free all the memory allocated by the entire structure.
Finally, I think better support for putting data on the stack instead of the heap from the beginnings of unix with alloca would also have prevented memory leaks somewhat.
You can fake handles by using additional memory on the allocated block, but it better be there for free(). Moving them around isn't a big problem, there are some allocators that ping back on the MMU with double indirection.
I never understood why C never got an higher level memory allocation API, after all, it isn't as if the 10 years of systems programming languages that preceeded it didn't had already NEW(type) as primitive.
The problem with alloca() is that C isn't Ada, so instead of an allocation error if the stack isn't big enough, you get a corrupted stack.
Maybe the 'stack' could be a linked list in the heap as well, then there wouldn't be a practical limit to its size. Alloca being not great is an implementation issue rather than an issue with the interface.
The author missed a benefit of `calloc` that it can avoid re-zeroing memory. For example if the allocator knows that it just got a fresh page from the OS (which has already zeroed this memory for security reasons) it can avoid doing another zero. This is not only a performance improvement but can avoid touching (and forcing the OS to actually allocate) all of the pages. Maybe the best approach here would be an extra `is_zeroed` (which can be a false negative but not a false positive) field on the return from the alloc call.
16 comments
[ 588 ms ] story [ 724 ms ] threadWith respect to your explanation, I imagine there are some cases (maybe something older?), but in my experience the norm for an int on a system with 32-bit pointers is also 32-bits.
PDP-11, where calloc most likely originated, had 16-bit integers and 16-bit pointers. Apparently, later PDP-11s could be split into 64k of code space and 64k of data space (like the Harvard architecture) but that would still use 16-bit pointers.
Also, throwing C++ code in when he's showing C is wrong on so many levels.
(I just remembered a similar discussion by Richard Heathfield in C Unleashed. I looked it up and it's on pages 285-286. He sets out to improve upon the design of realloc(). He concludes "that whoever designed realloc did know what they were doing, after all. There's a lesson there somewhere for overzealous wheel inventors.")
Fundamentally, though, I don't think that "malloc() and free() are missing some features that might improve them for my use case" is the same as "malloc() and free() are bad APIs." They're okay APIs, and they're like anything. Once you know what to expect, you barely think about it anymore.
I do think it would be interesting to hear more about the logic that inspired the original C developers to design the APIs as they did.
In my mind, a far more common use case calls for memory pools, where memory can be allocated and not explicitly tracked by the application. The memory pool gets freed at the end of some unit of computation. This interface is already nicely supplied by an Apache memory pool library.
I think a big flaw with malloc is that the returned values are memory addresses and not just handles. That means objects can't be moved around in memory once allocated. There's also no notion of types with malloc, just memory sizes in bytes. That makes heap fragmentation more of a problem. Plus if you have an allocated object containing a pointer to another allocated object there's no easy way to free all the memory allocated by the entire structure.
Finally, I think better support for putting data on the stack instead of the heap from the beginnings of unix with alloca would also have prevented memory leaks somewhat.
I never understood why C never got an higher level memory allocation API, after all, it isn't as if the 10 years of systems programming languages that preceeded it didn't had already NEW(type) as primitive.
The problem with alloca() is that C isn't Ada, so instead of an allocation error if the stack isn't big enough, you get a corrupted stack.