Memory Layout, Alignment & Object Representation
Understand struct padding and alignment requirements, optimize member ordering for size, learn about cache lines and false sharing, and use C++20 features like [[no_unique_address]] for compact layouts.
Alignment Requirements
Every type in C++ has an alignment requirement — the address where an object of that type must be placed must be a multiple of its alignment. This is a hardware requirement: most CPUs cannot efficiently access misaligned data, and some architectures (ARM, older SPARC) will fault on misaligned access.
Common alignments on x86-64:
- char: 1 byte
- short: 2 bytes
- int, float: 4 bytes
- double, long long, pointers: 8 bytes
- long double: 16 bytes (on some platforms)
A struct's alignment is the largest alignment of any of its members. The compiler inserts padding bytes between members and at the end of the struct to satisfy alignment requirements.
Struct Padding in Practice
The compiler inserts invisible padding to align each member. Member ordering can dramatically affect struct size.
#include <iostream>
#include <cstddef> // offsetof
// BAD: wasteful ordering — 24 bytes
struct BadLayout {
char a; // offset 0, size 1
// 7 bytes padding (align double to 8)
double b; // offset 8, size 8
char c; // offset 16, size 1
// 3 bytes padding (align int to 4)
int d; // offset 20, size 4
// 0 bytes tail padding
}; // total: 24 bytes
// GOOD: optimized ordering — 16 bytes
struct GoodLayout {
double b; // offset 0, size 8
int d; // offset 8, size 4
char a; // offset 12, size 1
char c; // offset 13, size 1
// 2 bytes tail padding (struct alignment = 8)
}; // total: 16 bytes
int main() {
std::cout << "sizeof(BadLayout): " << sizeof(BadLayout) << '\n'; // 24
std::cout << "sizeof(GoodLayout): " << sizeof(GoodLayout) << '\n'; // 16
// Verify offsets
std::cout << "BadLayout::a offset: " << offsetof(BadLayout, a) << '\n'; // 0
std::cout << "BadLayout::b offset: " << offsetof(BadLayout, b) << '\n'; // 8
std::cout << "BadLayout::c offset: " << offsetof(BadLayout, c) << '\n'; // 16
std::cout << "BadLayout::d offset: " << offsetof(BadLayout, d) << '\n'; // 20
// Savings: 33% smaller per object
// With 1 million objects: 8 MB saved
}alignas and alignof
alignof(T) queries the alignment of a type. alignas(N) requests a specific alignment for a variable or type. These are essential for SIMD, DMA buffers, and cache-line alignment.
#include <iostream>
#include <new> // std::hardware_destructive_interference_size
// Query alignment
static_assert(alignof(int) == 4);
static_assert(alignof(double) == 8);
// Force alignment
struct alignas(64) CacheAligned {
int data[16]; // 64 bytes of data on a 64-byte boundary
};
static_assert(alignof(CacheAligned) == 64);
static_assert(sizeof(CacheAligned) == 64);
// Align a variable
alignas(256) char simd_buffer[256];
// Over-aligned allocation (C++17)
struct alignas(128) BigAlign {
double values[8];
};
int main() {
// C++17: hardware_destructive_interference_size
// Minimum offset to avoid false sharing (typically 64 on x86)
// NOTE: not available on all compilers
// std::cout << std::hardware_destructive_interference_size << '\n';
auto* p = new CacheAligned; // C++17 guarantees aligned allocation
std::cout << "alignof(CacheAligned): " << alignof(CacheAligned) << '\n';
std::cout << "address: " << p << '\n';
std::cout << "aligned? " << (reinterpret_cast<std::uintptr_t>(p) % 64 == 0
? "yes" : "no") << '\n';
delete p;
}Cache Lines & False Sharing
Modern CPUs don't access memory byte-by-byte — they load entire cache lines (typically 64 bytes on x86). When two threads modify variables that happen to be on the same cache line, the hardware must constantly invalidate and reload that line between cores. This is called false sharing, and it can reduce multi-threaded performance by 10-100x.
False sharing occurs when:
- Two threads write to different variables
- Those variables are on the same 64-byte cache line
- The CPU's cache coherence protocol (MESI) forces constant cache line bouncing
The fix: pad or align hot variables to separate cache lines. Use alignas(64) or std::hardware_destructive_interference_size (C++17, but not supported on all compilers).
Conversely, true sharing is when multiple related variables are accessed together. Placing them on the same cache line improves performance — this is std::hardware_constructive_interference_size.
False Sharing Example & Fix
This example demonstrates false sharing: two counters on the same cache line cause massive slowdown when incremented by different threads.
#include <atomic>
#include <thread>
#include <chrono>
#include <iostream>
constexpr int ITERATIONS = 100'000'000;
// BAD: counters on the same cache line — false sharing
struct BadCounters {
std::atomic<int> counter1{0};
std::atomic<int> counter2{0}; // likely on same 64-byte cache line
};
// GOOD: each counter on its own cache line
struct GoodCounters {
alignas(64) std::atomic<int> counter1{0};
alignas(64) std::atomic<int> counter2{0}; // guaranteed separate cache lines
};
template <typename Counters>
void benchmark(const char* label) {
Counters c;
auto start = std::chrono::high_resolution_clock::now();
std::thread t1([&] { for (int i = 0; i < ITERATIONS; ++i) ++c.counter1; });
std::thread t2([&] { for (int i = 0; i < ITERATIONS; ++i) ++c.counter2; });
t1.join();
t2.join();
auto elapsed = std::chrono::high_resolution_clock::now() - start;
std::cout << label << ": "
<< std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count()
<< " ms\n";
}
int main() {
std::cout << "sizeof(BadCounters): " << sizeof(BadCounters) << '\n'; // 8
std::cout << "sizeof(GoodCounters): " << sizeof(GoodCounters) << '\n'; // 128
benchmark<BadCounters>("False sharing ");
benchmark<GoodCounters>("No false sharing");
}Bit Fields & [[no_unique_address]] (C++20)
Bit fields specify the exact number of bits a member occupies — useful for hardware registers and protocol headers, but their layout is implementation-defined. [[no_unique_address]] (C++20) allows empty members (stateless deleters, allocators) to occupy zero bytes by overlapping with other members, replacing the older Empty Base Optimization pattern.
#include <iostream>
#include <cstdint>
// Bit fields: pack flags tightly
struct TCPFlags {
uint8_t fin : 1;
uint8_t syn : 1;
uint8_t rst : 1;
uint8_t psh : 1;
uint8_t ack : 1;
uint8_t urg : 1;
uint8_t ece : 1;
uint8_t cwr : 1;
};
static_assert(sizeof(TCPFlags) == 1); // 8 flags in 1 byte
// [[no_unique_address]]: eliminate empty member overhead
struct Empty {};
struct Before { int x; Empty e; }; // sizeof == 8 (padding)
struct After { int x; [[no_unique_address]] Empty e; }; // sizeof == 4
static_assert(sizeof(Before) == 8);
static_assert(sizeof(After) == 4);
int main() {
TCPFlags tcp{};
tcp.syn = 1;
tcp.ack = 1;
std::cout << "SYN: " << tcp.syn << ", ACK: " << tcp.ack << '\n';
// Cannot take address of bit field: &tcp.syn is a compiler error
std::cout << "Before: " << sizeof(Before) << '\n'; // 8
std::cout << "After: " << sizeof(After) << '\n'; // 4
}1. Never use memcpy to serialize structs across platforms — padding and member alignment differ between compilers and architectures.
2. Bit field portability: The order of bits within a byte and padding between bit fields are implementation-defined. Do not use bit fields for cross-platform wire formats — use explicit bit manipulation.
3. #pragma pack is non-standard: Packing structs to remove padding can cause misaligned access (performance loss or crashes on strict-alignment architectures like ARM).
4. Order members largest-to-smallest: double, pointers (8-byte), then int (4-byte), then short (2-byte), then char (1-byte). This minimizes internal padding. For hot structs, use static_assert(sizeof(MyStruct) <= 64) to enforce cache-line-friendly sizes.
- Every type has an alignment requirement — the compiler inserts padding to satisfy it
- Order members from largest to smallest alignment to minimize padding
- Cache lines are 64 bytes on x86 — false sharing between threads is a major performance trap
- Use
alignas(64)to put per-thread data on separate cache lines [[no_unique_address]](C++20) eliminates overhead of empty members- Bit fields pack data tightly but have implementation-defined layout — don't use for wire formats
Quiz — Test Your Knowledge
(15 XP)1. Given `struct S { char a; double b; char c; };`, what is the likely sizeof(S) on a 64-bit platform?
2. What is false sharing?
3. What does `[[no_unique_address]]` (C++20) do?