About alignment, struct layout and the cache...

published at 24.09.2026 15:57 by Jens Weller
Save to Instapaper Pocket

Starting a new series on the blog. One result of Meeting C++ 2025 was to generally focus more on my own content, then just giving others support for their posts. So I've spend some time creating content to be now released in front of Meeting C++ 2026.

A good overview on this topic are the two talks by Jonathan Müller: his talk on Cache friendly C++ from Meeting C++ 2025, and his talk on writing cache friendly C++ from Meeting C++ 2018. I got to watch his talk during the 24h++ event I’ve organized in December 2025, around this time also a question about padding bytes was asked on a LinkedIn comment in one of my posts. But also because I think this is something which gets often overlooked by many. That includes myself.

Struct layout is intertwined with alignment. Alignment allows (or demands) the representation of variables in the memory in a certain way that they are accessible and addressable in a hardware friendly manner. So the members of a struct are put in certain memory locations, and the room between them is filled with padding bytes. That’s why ordering your member variables in a struct alphabetically or grouping them by certain criteria might make your types larger then they could or should be. And grouping member variables is always a great hint that they should be a type of their own. There are also some ways of influencing this like using pragma pack, __attribute__((packed)) or using bitfields (int x:2;) in order to achieve smaller type size. But then you'll ignore the reasons that exist for alignment in the first place. Lets revisit alignment after looking at struct layout.

And in case you wonder why would anyone order members alphabetically in a struct or class? Its easy to see this as an internal guideline for things like readability. Or generated glue code from certain formats such as JSON, where often implementations store objects in a form of string:value – and then representing this in map sorting by the string. In similar ways the ordering of members in a struct might align with how they are defined in a standard. This might be grouped by the standards logic, or the table is sorted alphabetically.

For struct layout, ordering by actual memory size decreasing gives the best results in term of size:

struct A{char c; int i; float f; bool b;};
struct B{int i; float f; char c; bool b;};

When using sizeof you can see the actual size of a type. Adding together the sizes of its members both structs have the size of 10 bytes, but struct a yields 16 bytes and b is 12 bytes according to sizeof. When you change the int to size_t (8 bytes size), struct a is now 24 bytes, while b grows by only 4 bytes to 16. As a type needs to align with the needs of its first member in an array, padding bytes are also added to the end of the type if needed, not just between the types of struct/class members. That way a struct aligns correct in memory as an array.

A great way to explore this is the tool pahole, which is also available as a tool in Compiler Explorer, see the example code. Also C++ insights has an option to show you the padding of a struct or class, Visual Studio even has a memory layout view where you can see the layout of your own types. But you should be aware when you make changes to the code which is already in production, that reordering the members of a class can be a breaking change. Your memory layout (ABI) of that type changes, and is now also represented differently in binary (e.g. compiled) form when distributed.

Looking into this made me realize, that this is actual the first use case for reflection in C++26 I might have. Right now its ideal for playing around in Compiler Explorer, though reflection support is now available with GCC 16. Great idea, I thought. Though my first learning about reflection in C++26 is, that it has iterated through various stages, and what you find online (at least in early 2026) may show you these older, now “deprecated” approaches, as for certain operations now a context argument is required, while in the past you could just get the list of members from a type as the single argument.

My first attempt failed, but when reading the example section of the newest proposal for reflection (revision 13), I’ve noticed that the reflection code for struct layout was one of the examples. Reading this helped with getting my own example to run. After some refactoring I got to this code example of a consteval function computing an std::array showing the memory mapping of a Type:

template< class Type>
consteval auto showTypeLayout() -> std::array<int,sizeof(Type)>
{
    std::array<int,sizeof(Type)> arr;
    arr.fill(-1);
    constexpr auto ctx = std::meta::access_context::current();
    auto members = std::meta::nonstatic_data_members_of(^^Type, ctx);
    size_t i = 0;
    for(auto& element:members)
    {
        size_t s = static_cast(std::meta::offset_of(element).bytes);
        size_t e = s + std::meta::size_of(element);
        do
        {
            arr[s]=i;
        }while(++s < e);
        ++i;
    }
    return arr;
}

This code first creates an std::array of int with the size of the type in bytes and then fills this array with the value -1. One learning from this is that std::array does not have a constructor to do this, but a member function fill. Then it uses C++26 reflection to iterate over each member and retrieve its offset and size information for the underlying type. This is used to write the index of the member into the std::array.

The code is then used in a print function for ease of use:

template < class T>
void print_type()
{
    std::println("{}: {} bytes {}", std::meta::identifier_of(^^T), sizeof(T),showTypeLayout());
} 

This simple helper function prints a line with the type name as returned by identifier_of, the size and the array returned from showTypeLayout();

Which then is tested with various types:

struct A
{
char c;
size_t x;
float f;
bool b;
};

struct B
{
size_t x;
float f;
char c;
bool b;
};

struct C
{
int x;
B b;
};

struct S { unsigned i:2, j:6; };

int main()
{
print_type<A>();
print_type<B>();
print_type<C>();
print_type<S>();
}

Leading to the output of:

A: 24 bytes [0, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, -1, -1, -1]
B: 16 bytes [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 3, -1, -1]
C: 24 bytes [0, 0, 0, 0, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
S: 4 bytes [1, 1, 1, 1]

In this output -1 represents a padding byte, while the code writes the member index into the array for each member of the struct. The last two structs test the boundaries of this, as member objects are not resolved by the code and if a byte shares two or more members, the prior values are overwritten. I also think that the sizeof in this instance shows the type, not the bit fields. You can find the code on Compiler Explorer.

C++26 enables to do this without using libraries like boost::pfr, which could achieve similar and offer some limited ways of implementing reflection code prior to C++26.

These padding bytes exist in order to align the various types in memory, the alignment requirement of the type is met. Type A shows that the next member is placed at its next alignment possibility. size_t has an alignment of 8 bytes, so it needs to align with this as a boundary condition for memory placement. Hence there are 7 padding bytes after its first member. size_t is also 8 bytes, but sizeof is not the alignment requirement, alignof(Type) allows one to query a types alignment requirement. For the test structs above this is 8 bytes for ABC, and 4 bytes for S.

C++ runs on an abstract machine, and to fully understand alignment one has to look beyond this into how the code actually is executed on the processor. Registers, busses and cache lines, and alignment requirements, which the compiler and C++ fulfills for you. A misaligned type leads to slower execution (x86-64), an error (ARM64) or error/simulation (RISC-V). This blog goes deeper into the hardware constraints of alignment in C++. While this article focuses on how alignment is implemented and represented through out the chain of compilation from code to object file.

In this context also a quick word about std::aligned_storage, which is until C++23 a type that allows you to allocate uninitialized storage which is correctly aligned for a Type/alignment parameter. Objects who meet the alignment requirement and size of the storage can be allocated then with placement new. A similar type exists for unions of types with std::aligned_union. Both types are deprecated with C++23, so you should not use them anymore.

Jason Turner gives in C++ weekly 410 a good overview on padding and alignment.

Lets look again at the padding of struct A and B:

A: 24 bytes [0, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, -1, -1, -1]
B: 16 bytes [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 3, -1, -1]

The difference in size is 8 bytes, which are only used for padding due to struct A choosing a different order for its member types. The padding after the last member is to align the next type again correctly, which in an array would be the same type. A smaller type fits more often into a given memory size.

Ordering types by the size in your structs is a good guideline to improve performance, if it reduces the size of the type. In performance critical contexts you should always measure which impact a reordering of member variables has for a type. As when you have struct with int x,y and size_t counter, both {x,y,counter} and {counter,x,y} will be optimal in regards for padding. In larger types that may span multiple cache lines, reordering members should have in mind that variables often accessed together benefit from being in the same or adjacent cache line. In this case moving a smaller type to the end while putting an array to the front of the type may lead to loss in performance.

Which brings us to caches and cache lines. The C++ standard defines the size of a cache line through two constants: std::hardware_destructive_interference_size, for the minimum distance in memory to not be shared within one cache line to avoid false sharing. There is also std::hardware_constructive_interference_size, which defines the size of a cache line in the sense of the minimum distance of true sharing – e.g. the size of a cache line. Both are often the same value of currently 64 or 128 bytes for a cache line. The first one is used when you’d like to keep things apart in memory, so that they are not in the same cache line. A good example are indexes for producer and consumer queues, where its better if these are not during the memory access through two threads in the same cache line in memory for performance reasons:

alignas(std::hardware_destructive_interference_size) std::atomic producer_index;
alignas(std::hardware_destructive_interference_size) std::atomic consumer_index;

In this example alignas is used to ensure the placement of each variable in its own cache line. When one thread accesses the producer_index, it does not load also the consumer_index with that cache line. In an ideal world the consumer and producer thread then do not share these objects through accessing the same cache line. If they would, false sharing would cause a slow down through synchronization, even if they don’t access the same variables.

The second one allows you to check if a type fits into a cache line, a static_assert can be used to trigger a compilation error in this case.

While L1/2/3 caches have various sizes in your processor, the cache line is the block of memory that gets loaded to access memory in the processor. Its usually 64 (or 128) bytes that are accessed, and when the next memory access is within the cache or same cache line, the processor does not need to load that variable from memory, its already in the cache. That’s why smaller types and arrays of types are faster in processing. As they have a cache friendly layout.

For this there is an example for this in the standard with std::mutex under Windows. When C++11 released, the standard mutex implementation for MSVC still had to support some older Windows systems, and for that reason is larger then it would need to be today. This is a Windows specific detail, GCC or clang under Linux or Mac are not having this specific issue. Later with C++17 std::shared_mutex was introduced to the standard, which is now on MSVC smaller then std::mutex, as it was released at a time when the backward compatibility for such older systems was dropped. Its smaller by a factor of 10, this simple line of showing the sizes of mutex and shared mutex:

std::cout << "mutex:" << sizeof(std::mutex) << " shared_mutex:" << sizeof(std::shared_mutex);

Shows the following results on Compiler Explorer:

x64 msvc v19.44 VS17.14: mutex:80 shared_mutex:8
x86-64 gcc 15.2: mutex:40 shared_mutex:56
x86-64 clang 21.1.0: mutex:40 shared_mutex:56

This table shows you that using shared_mutex under Windows even for non shared use cases could be a good idea. The std::mutex variable does not even fit into a cache line.

A good comparison for cache efficiency is vector vs. list. While vector stores its memory in a continuous array in memory, a list chooses to allocate a node for every new entry. This leads to more cache misses in a list, and even in the perfect world where all list entries are aligned in an array in memory - these still contain the pointers to the next and prior element. Taking up space.

Today's execution environment has become very much shaped by the cache and cache line size, optimal code makes good use of this with trying to group often accessed variables together and preferring continuous memory structures such as array, string and vector.

Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!