The one thing Rust got right was inheriting the i64/u64/i32/u32/i8/u8 naming convention for types. Far more concise and unambiguously clear. No more typing unsigned long long int or uint64_t
I would have liked it more if it was U64 instead of u64, etc. It feels weird when all the other types are in PascalCase but then the primitive types are like this. Also str -> Str.
It's ok as long as the types don't leak into the public API when the code is distributed as a library. But yeah, it looks like a leftover bad habit from pre-C99 times (I suspect the author is on Windows where MSVC was very late to the C99 party).
> This will be used to align the data. For some reason, our computers are much better at dealing with data that is aligned at 8 bytes. This means that the start of each new allocation has to be at a multiple of 8 bytes.
Casting a pointer which is not aligned to its natural alignment has an undefined behavior in C. It's not just an issue of unaligned accesses being slow; in practice, your program might also crash or access unexpected data on real platforms with real compiler implementations.
There's really no other way for a general allocation function that doesn't know the target type. I would expand the default alignment from 8 to 16 bytes though, because this is what you typically get from malloc on 64-bit targets, and a bigger 'natural alignment' should be extremely rare (maybe needed for some SIMD types, but those are outside the C standard anyway).
> It's not just an issue of unaligned accesses being slow; in practice, your program might also crash or access unexpected data on real platforms with real compiler implementations.
Unaligned accesses are fast (unless they straddle a cache line) and work perfectly well on current mainstream CPUs (and have been working fine for a long time).
E.g. I'm really not a fan of taking every piece of C code apart because it might not work on an experimental CPU design from the 1960s which the C standard theoretically supports but no current C compiler ;)
“Types.c” is dangerous code to bring to every project.
f32/64 makes assumptions about the platform.
Worse yet, the macros at the bottom are all functionally incorrect. Macros like MIN/MAX need to be contained in parentheses to avoid precedence errors.
For example, we take `Min(x,y)` from the code. Consider what happens if I write
`ans = 2 * Min(3,4)` because of the way Min is defined that will expand to `ans = 2 * 3 < 4 ? 3 : 4` and while you may expect ans to be 3 it will infact resolve to 4.
Subtle bugs like these can creep into code and cause really hard to debug errors.
Finally, while u32 is a little faster to type than `uint33_t` it makes the project harder to read forcing the reader to learn the author’s personal c dialect. While it is a stylistic choice, I can’t see why anyone would want to sacrifice code highlighting and expressiveness for slightly less verbosity
> forcing the reader to learn the author’s personal c dialect
If you open my codebase and understanding the u32 typedef is enough to trip you up to the point where you're now "learning a new C dialect", you're probably not well equipped to understand the actual complex stuff.
God forbid people make the slightest stylistic choice about how the code they're staring at for 10 hours a day looks.
How would I handle memory fragmentation in this implementation of an Arena Allocator. If I do not need a piece of memory any more, is there a way to free it and possible use it for something else.
Arena allocator cannot free. so it's usecase is less general than a normal allocator.
But imagine creating a readonly tree, working with that tree, and then disposing of the tree. This is where a arena allocator shines. It removes fragmentation almost entirely from the main allocator.
Or you can just use alignof(max_align_t), and your pointers will have the same alignment guarantees as malloc.
And I might be wrong, but even if you do accept a required alignment `align`, I think it's impossible in portable C to align the pointer `p` to it, unless you assume something not-strictly-portable about (uintptr_t)p, or `align` is less than some predetermined alignment that you used to allocate the char array.
The question is, even after so many years, why is this arena memory allocation management not part of std library ? At least for people to use it safely, given the fact that writing this safely and portably is non-trivial.
36 comments
[ 1.7 ms ] story [ 40.8 ms ] threadAlso, your code is prone to overflow when you add data, used, alignment and size.
And finally, the realloc function could be reworked to do nothing if new_size is <= old_size.
Casting a pointer which is not aligned to its natural alignment has an undefined behavior in C. It's not just an issue of unaligned accesses being slow; in practice, your program might also crash or access unexpected data on real platforms with real compiler implementations.
> It's not just an issue of unaligned accesses being slow; in practice, your program might also crash or access unexpected data on real platforms with real compiler implementations.
Unaligned accesses are fast (unless they straddle a cache line) and work perfectly well on current mainstream CPUs (and have been working fine for a long time).
E.g. I'm really not a fan of taking every piece of C code apart because it might not work on an experimental CPU design from the 1960s which the C standard theoretically supports but no current C compiler ;)
- The macros doing arithmetic (KiB, MIN etc) don't put their arguments in parentheses, that is very dangerous.
- The code assumes that int and i32 are the same size.
- Don't cast the return value of malloc(). Please Google this.
I appreciated the use of structs by value, that's a nice design.
I agree there's a risk of overflow in the arithmetic around sizes/alignment, but with a 64-bit `usize` that should be mostly theoretical.
Tbf this is extreme nitpicking. It's very unlikely that the code will ever be built on a compiler where int is not 32 bits.
> Don't cast the return value of malloc(). Please Google this.
This is required when the code should also build as C++ (which some weirdos seem to prefer).
> The macros doing arithmetic (KiB, MIN etc) don't put their arguments in parentheses, that is very dangerous.
Agreed.
Declare your function "static inline" in the header file and the compiler will usually inline it with -O2 level optimisations.
You can even integrate the struct declaration right into the return type:
https://www.godbolt.org/z/GGraK61M6
(which tbf though has limited usability)
f32/64 makes assumptions about the platform.
Worse yet, the macros at the bottom are all functionally incorrect. Macros like MIN/MAX need to be contained in parentheses to avoid precedence errors.
For example, we take `Min(x,y)` from the code. Consider what happens if I write `ans = 2 * Min(3,4)` because of the way Min is defined that will expand to `ans = 2 * 3 < 4 ? 3 : 4` and while you may expect ans to be 3 it will infact resolve to 4.
Subtle bugs like these can creep into code and cause really hard to debug errors.
Finally, while u32 is a little faster to type than `uint33_t` it makes the project harder to read forcing the reader to learn the author’s personal c dialect. While it is a stylistic choice, I can’t see why anyone would want to sacrifice code highlighting and expressiveness for slightly less verbosity
Indeed, but, by the way, those are assumptions that some newer languages now make about the entire world.
If you open my codebase and understanding the u32 typedef is enough to trip you up to the point where you're now "learning a new C dialect", you're probably not well equipped to understand the actual complex stuff.
God forbid people make the slightest stylistic choice about how the code they're staring at for 10 hours a day looks.
This is just standard gatekeeping by pearl clutching devs with PTSD from decades ago.
But imagine creating a readonly tree, working with that tree, and then disposing of the tree. This is where a arena allocator shines. It removes fragmentation almost entirely from the main allocator.
This is unportable. You really need to pass the required alignment to the allocation function.
And I might be wrong, but even if you do accept a required alignment `align`, I think it's impossible in portable C to align the pointer `p` to it, unless you assume something not-strictly-portable about (uintptr_t)p, or `align` is less than some predetermined alignment that you used to allocate the char array.
What's not strictly portable about it?
The only assumption here is that align is a power of 2, which is guaranteed if it's the result of an alignof().