> I imagine just about any computer science major would have learned the rules of memory layout according to some kind of C-like compiler.
I have worked with a number of fresh grads over the last ten years. I can think of one who may have had a good handle on this. At best the rest range from “vague memory recall about this” to a blank stare.
On the flip hand, it’s something someone can pick up pretty quickly if motivated.
I've been learning Zig, and needed a refresher on memory layout (@sizeOf and @alignOf).
Wrote this blog post to summarize what I think are the right ways to understand alignment and size for various data types in Zig, just through experimentation.
> CPUs fetch data from memory in fixed-size blocks of so-many bytes, and performance degrades when data is misaligned.
A memory bus supports memory transactions of various sizes, with the largest size supported being a function of how many data lines there are. The following two statements are true of every memory bus with which I'm familiar, and I probably every bus in popular use: (1) only power-of-two sizes are supported; (2) only aligned transactions are supported.
Arm, x86, and RISC-V are relatively unique among the multitude of CPU architectures in that if they are asked to make an unaligned memory transaction, they will compose that transaction from multiple aligned transactions. Or maybe service it in cache and it never has to hit a memory bus.
Most CPU architectures, including PPC, MIPS, Sparc, and ColdFire/68k, will raise an exception when asked to perform a misaligned memory transaction.
The tradition of aligning data originated when in popular CPU architectures, if you couldn't assume that data was aligned, you would need to use many CPU instructions to simulate misalinged access in software. It continued in compilers for Arm and x86 because even though those CPUs could make multiple bus transactions in response to a single mis-aligned memory read, that takes time and so it was much slower.
I don't know for sure, but I would expect that on modern x86 and high performance Arm, the performance penalty is quite small, if there's any at all.
i could be wrong but i believe the zig compiler reserves the right to lay things out differently depending on compilation mode? especially debug. unless it's extern or packed, in which case the layout will be defined.
`extern` and `packed` container types have well defined layouts. a regular `struct` is an "auto" layout - and the compiler can and will rearrange whenever it wants.
if you need a well defined layout, use `extern`. if your struct makes sense to represent as an integer, use `packed`. I think it is often ill advisable to use `packed` otherwise.
you can explore this yourself on the Type info returned from @TypeInfo(T):
Highly recommend not doing this in production code. If nothing else, there's no compiler protection against offset+size being > total size, but one could add it with a static assert! (I've done so in the godbolt link)
You might want to have a look at the unboxing and packing annotations that are proposed for Virgil. The unboxing mechanism is implemented and there was a prototype of the packing mechanism implemented by Bradley for his thesis. I am working on making a more robust implementation that I can land.
I also had to learn struct alignment the hard way working on WebGPU path tracer and struggling to understand why struct fields not aligning (ironically).
> I imagine just about any computer science major would have learned the rules of memory layout according to some kind of C-like compiler.
I have worked with a number of fresh grads over the last ten years. I can think of one who may have had a good handle on this. At best the rest range from “vague memory recall about this” to a blank stare.
On the flip hand, it’s something someone can pick up pretty quickly if motivated.
I've been learning Zig, and needed a refresher on memory layout (@sizeOf and @alignOf).
Wrote this blog post to summarize what I think are the right ways to understand alignment and size for various data types in Zig, just through experimentation.
Let me know any and all feedback!
> CPUs fetch data from memory in fixed-size blocks of so-many bytes, and performance degrades when data is misaligned.
A memory bus supports memory transactions of various sizes, with the largest size supported being a function of how many data lines there are. The following two statements are true of every memory bus with which I'm familiar, and I probably every bus in popular use: (1) only power-of-two sizes are supported; (2) only aligned transactions are supported.
Arm, x86, and RISC-V are relatively unique among the multitude of CPU architectures in that if they are asked to make an unaligned memory transaction, they will compose that transaction from multiple aligned transactions. Or maybe service it in cache and it never has to hit a memory bus.
Most CPU architectures, including PPC, MIPS, Sparc, and ColdFire/68k, will raise an exception when asked to perform a misaligned memory transaction.
The tradition of aligning data originated when in popular CPU architectures, if you couldn't assume that data was aligned, you would need to use many CPU instructions to simulate misalinged access in software. It continued in compilers for Arm and x86 because even though those CPUs could make multiple bus transactions in response to a single mis-aligned memory read, that takes time and so it was much slower.
I don't know for sure, but I would expect that on modern x86 and high performance Arm, the performance penalty is quite small, if there's any at all.
i could be wrong but i believe the zig compiler reserves the right to lay things out differently depending on compilation mode? especially debug. unless it's extern or packed, in which case the layout will be defined.
`extern` and `packed` container types have well defined layouts. a regular `struct` is an "auto" layout - and the compiler can and will rearrange whenever it wants.
if you need a well defined layout, use `extern`. if your struct makes sense to represent as an integer, use `packed`. I think it is often ill advisable to use `packed` otherwise.
you can explore this yourself on the Type info returned from @TypeInfo(T):
https://ziglang.org/documentation/master/std/#std.builtin.Ty...
https://ziglang.org/documentation/master/std/#std.builtin.Ty...
https://ziglang.org/documentation/master/std/#std.builtin.Ty...
7 replies →
I know this is a bit cursed; but, I always wanted a bitfield-on-steroids construct:
It is a bit cursed, but you can do this in C/C++.
https://godbolt.org/z/vPKEdnjan
The member types don't actually matter here so we can have a little fun and macro it without having to resort to templates to get "correct" types.
Highly recommend not doing this in production code. If nothing else, there's no compiler protection against offset+size being > total size, but one could add it with a static assert! (I've done so in the godbolt link)
Edit: if you're talking about Zig, sorry!
You might want to have a look at the unboxing and packing annotations that are proposed for Virgil. The unboxing mechanism is implemented and there was a prototype of the packing mechanism implemented by Bradley for his thesis. I am working on making a more robust implementation that I can land.
https://arxiv.org/abs/2410.11094
I'm not sure I understand your example; if I am looking at it right, it has overlapping bitfields.
But supposing you didn't want overlapping fields, you could write:
And the compiler would smash the bits together (highest order bits first).
If you wanted more control, you can specify where every bit of every field goes using a bit pattern:
Where each of T, b, z, and r represent a bit of each respective field.
Are you saying you want foo and bar to completely overlap? And baz and foo / bar to partially overlap? And have lots of unused bits in there too?
C# can do this with structs. Its kind of very nice to unpack wire data.
I think you can do this with Virgil, but I'm having trouble finding the exact doc page at the moment: https://github.com/titzer/virgil
The description is in the paper, but not all of it is implemented.
https://arxiv.org/abs/2410.11094
Bradley implemented a prototype of the packing solver, but it doesn't do the full generality of what is proposed in the paper.
You can kinda do this with Zig’s packed structs and arbitrary-width integers
Look at Erlang bit syntax: https://www.erlang.org/doc/system/bit_syntax.html
It can even be used for pattern matching.
I don't know whether Gleam or Elixir inherited it.
Memory layout of a data structure in various programming languages: https://rosettacode.org/wiki/Memory_layout_of_a_data_structu...
I also had to learn struct alignment the hard way working on WebGPU path tracer and struggling to understand why struct fields not aligning (ironically).
useful!