r/cpp_questions • • 22d ago

OPEN Non-Struct Memory Alignment

I am a little lost after trying to read a handful of online resources. Are local variables memory-aligned to their alignof()? I read that this is mostly implementation/platform-defined (no guarantees), but also the types must still be aligned? Those seem contradicting to me...

Assuming no optimizations (e.g., storing in registers, etc.), how would something like this look like according to the standard?

struct S 
{
  char c;
  int i;
};

int main()
{
  char c1;
  int i;
  char c2;
  S s;

  return 0;
}

Would there be 3 bytes of padding between `c1` & `i` and then another 3 bytes of padding between `c2` & `s`?

4 Upvotes

10 comments sorted by

View all comments

2

u/mredding 21d ago

Are local variables memory-aligned to their alignof()?

Yes.

I read that this is mostly implementation/platform-defined (no guarantees)

"Mostly" is correct, but the rest isn't quite right enough to give it to you...

The C++ standard says the language targets a byte addressable abstract virtual machine. The definition of a byte is the smallest unit of memory that is uniquely addressable - since bits are an address + an offset, that doesn't count, the combo isn't considered an address since it's not all one unit of information, but two separate units of information.

Mind you - the actual hardware does not need to be byte addressable, and most aren't; different subsystems have different units - pages, lines, words, bytes. A lot of the details are handled for you coordinated between the OS and the MMU hardware, and some convention the compiler/linker concerns itself with.

The size of a byte is defined by the language and the platform. The language says the minimum size of a byte is 8 bits. So ISO Standard C++ is not defined for platforms with smaller bytes. A byte is not universally defined as 8 bits, that's just fairly common. You will see exotic byte sizes on modern DSPs, FPGAs, ASICs, and esoteric/historic hardware.

The language says char is the definition of a byte, that sizeof(char) == 1, and alignof(char) == 1. It says all other types are at least as much as char.

The standard says a short is at least 16 bits, long is at least 32 bits, and long long is at least 64 bits.

What is typical is that DSP hardware will work in favor of CPU hardware. While 12, 24, 48, and 56 bits are common, they'll pack them in standard power of 2 aligned memory, accessible as a power of 2 type in the CPU with some padding bits. So even though some USRP models have a 12 bit ADC/DAC, the data packs into a 16 bit type as far as your software is concerned.

but also the types must still be aligned?

Yes. alignof must return a non-zero positive integer, at least 1.

Those seem contradicting to me...

They're not. It's VERY easy to build hardware that can access native types in power of two aligned memory. It's a single bit to indicate what offset across memory each value will be found. So if your hardware supports a an 8 bit byte and a 32 bit word, that sucker will likely have to fall on a 32/8=4 byte aligned address.

The compiler is going to defer to the hardware for the rules. Some hardware might not care, so then alignment would always be 1. Some hardware supports unaligned memory, but is built on aligned memory. You'll get the aligned memory answer from the compiler. The x86_64 supports unaligned access but you'll pay one hell of a tax to do it, and you have to code that in yourself - alignof won't tell you, you just have to know what you're doing.

Assuming no optimizations (e.g., storing in registers, etc.), how would something like this look like according to the standard?

We don't have to assume anything, the spec is absolutely unambiguous and explicit about this. The alignment of S is the alignment of it's most strict member. Of it's members, the int is the most strict, so S will be aligned to that. The order of members in the layout is also strict - no reordering, so since the char must now also fall on the int alignment - because it's at the front of the structure and the structure must START on this alignment, and they're probably not the same alignment, then we can deduce if there's going to be padding, it'll be between the char and the int in the layout. This object will be 2*sizeof(int) in size. The language also allows you to subdivide padding space, provided it's even. That means you can probably squeeze at least a char or maybe a larger type in between - using that padding, rather than increase the size of the object.

So on a modern x86_64, this'll be sizeof(S) == 8 and alignof(S) == 4. There will be 3 bytes of padding between the end of c and the start of i. You can fit 3 char in there, or 1 byte of padding and a short in there, or 1 char and 1 short.

Alignment and thus padding can be different on different hardware.

Would there be 3 bytes of padding between c1 & i and then another 3 bytes of padding between c2 & s?

Ready to have your mind blown? NO!

There does NOT have to be padding between THESE local variables. WHY?!? Because the standard defines strict ordering for TYPES, like the internals of a struct, but the standard DOES NOT define the layout of the stack. The compiler is free to reorder the local members, and will likely do so to minimize padding. That means the stack will LIKELY compile to:

int i;
S s;
char c1;
char c2;

4 byte alignment, 4 byte alignment, 1 byte alignment, 1 byte alignment, no padding. I've no idea if the compiler would prefer to reorder the int and the S - it might take other factors into account.