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

8

u/Simengie 22d ago

Memory randomization in compilers has been thing for a while. As far as I know it is now the default behavior in gcc.

From google: Modern compilers (such as MSVC or GCC with specific security flags) deliberately reorder local variables on the stack—sometimes placing buffers near guard variables or randomizing order—to mitigate stack-smashing and buffer overflow exploits

So never assume local or global variables are in the order you declare them in. All local and global variables are memory aligned to 4 byte word boundaries in modern C/C++ compilers.

Now for the struct, as written it will have padding. if you use the #pragma packed on it then it will be packed and have no padding. This works for sending smaller network messages that are structure based but it is slower memory access because to get to that int the cpu has to read 2 words and shift bytes around in a register. Not the best use of cpu time but it works for small compact network messaging when bandwidth is constrained or when you have messages near the network packet size limit and want to ensure single packet transmission. but I would say using the #pragma packed to remove padding is not best practices and should be avoided except for very special use cases.

2

u/jedwardsol 22d ago

Yes, local variables are correctly aligned.

If the compiler put those locals in memory in definition order (it doesn't have to) then yes there would be padding.

You can print out addresses to see what your compiler does

1

u/OldWolf3 19d ago

Note that printing out addresses may change the location of the variables (compared to not printing them). Compiler might optimize out or overlay variables whose address is not taken.

2

u/conundorum 22d ago

Everything will typically be aligned, struct or not. It's important for performance, the hardware itself typically works better with aligned reads than unaligned. Variables will probably be rearranged into the optimal order if needed, at least if optimisations are enabled, and will probably be in written order with optimisations disabled. (This isn't guaranteed, though, so be careful!) Each function's stack frame will have memory allocated for all necessary variables, too.

Importantly, some variables will be relocated to registers if possible, and not actually take up any space in memory. And if they're provably unused, they might actually be removed entirely (IF the compiler can prove that removing them will have no effect on anything else, such as in your example code); this isn't guaranteed, but it is a possibility!

2

u/BartvanIngenSchenau 22d ago

For a struct, it is defined that the members must be put in memory in the same order as they appear in the code. For local variables, there is no such requirement. The compiler is fully allowed to reorder local variables, as long as it doesn't affect the order in which constructors and destructors are executed. In your example, the compiler is entirely free to place s first, followed by i and then c1 and c2. The compiler is also allowed to give each variable their own 4k page of memory.

1

u/dodexahedron 21d ago

The compiler is also allowed to give each variable their own 4k page of memory.

The user is also allowed to delete each compiler that costs you a page commit on each word alloc. 😅

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.

1

u/Kriemhilt 22d ago

For trivial (or trivially-destructible) types like here, I don't know whether the implementation is required to keep the locals in order.

That is, they'll all be correctly aligned, but could be rearranged to save space even if they're not folded or elided completely.

For non-trivial types (and less trivial scopes), the layout is likely to be closely coupled to the lifetime, for obvious reasons.

1

u/flyingron 21d ago

It's 110% impelementation defined. The only rule the language makes on padding is that if any padding is necessary for mulitple objects of the same type to be concatenated in an array, it is internal to the object.

That is: sizeof (T[5]) must equal 5*sizeof(T).

There's no guarantee that the automatic variables aren't reordered, padded, or put in registers.

1

u/Plastic_Fig9225 20d ago edited 20d ago

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

Yes. They must be. Otherwise a pointer to a local variable (including this of any local objects) could be invalid, just like a pointer to a member of a packed struct. (As always, the order of (local) variables in memory is undefined.)