Alignment and Layout
In C++, sizeof(T) is rarely equal to the sum of the sizes of its members. The compiler inserts Invisible bytes known as Padding between members to satisfy Alignment Requirements.
Understanding alignment is critical for systems programming for three reasons:
- Memory Footprint: Poorly ordered structures can waste memory on padding.
- Hardware Faults: Architectures like ARM, SPARC, and specialized DSPs raises hardware exceptions (SIGBUS) upon misaligned memory access.
- Concurrency Performance: Unintentional sharing of cache lines (False Sharing) can degrade multi-threaded performance.
1. Natural Alignment
CPUs access memory in words ( 4 or 8 bytes). Accessing a 4-byte integer located at address 0x1001 requires the CPU to perform two memory fetches (fetching 0x1000 and 0x1004) and Bit-shift the results. To prevent this inefficiency, types have a Natural Alignment.
The Rules of Alignment
- Fundamental Types: A type of size requires an address divisible by .
char(1 byte): Alignment 1 (Any address).int32_t(4 bytes): Alignment 4 (Address ending in 0, 4, 8, C).double(8 bytes): Alignment 8.
- Structures: The alignment of a
structis equal to the alignment of its strictest member. - Arrays: An array
T[N]has the same alignment asT.
Inspection Tools
C++11 provides operators to query these properties at compile time.
#include <iostream>
struct Example \{
char a;
double b;
int c;
\};
int main() \{
std::cout << "Size: " << sizeof(Example) << "\n";
std::cout << "Alignment: " << alignof(Example) << "\n";
\}
2. Structure Padding and Layout
To enforce alignment, the compiler injects padding bytes.
Case Study: The Unoptimized Layout
Consider the following structure on a 64-bit system (LP64 model).
struct BadLayout \{
char a; // 1 byte
// [ 7 bytes Padding ] -> To align 'b' to 8-byte boundary
double b; // 8 bytes
int c; // 4 bytes
char d; // 1 byte
// [ 3 bytes Padding ] -> To align structure size to 8
\};
- Calculated Size: bytes.
- Actual Payload: bytes.
- Wasted Space: 10 bytes (41%).
The Tail Padding Rule
The total size of a structure must be a multiple of its alignment. This ensures that if the struct Is used in an array, the second element properly aligns.
alignof(BadLayout)is 8 (due todouble b).- Therefore,
sizeof(BadLayout)must be a multiple of 8.
Layout Optimization
To minimize size, members should be ordered by alignment requirement ( size), from largest To smallest.
struct GoodLayout \{
double b; // 8 bytes (Offset 0)
int c; // 4 bytes (Offset 8)
char a; // 1 byte (Offset 12)
char d; // 1 byte (Offset 13)
// [ 2 bytes Padding ] -> To align size to 8
\};
- New Size: bytes.
- Savings: 8 bytes per instance.
[[no_unique_address]] (C++20)
Empty classes in C++ normally take up 1 byte (to ensure unique addresses). When used as members, This forces padding. The C++20 attribute [[no_unique_address]] allows the compiler to overlap the Empty class with the padding of other members.
struct Empty \{\}; // Size 1
struct Optimized \{
int i;
[[no_unique_address]] Empty e; // Size 0 effectively
\};
// sizeof(Optimized) == 4
3. Manual Alignment Control (alignas)
Sometimes, natural alignment is insufficient. This occurs in two scenarios:
- SIMD: SSE/AVX registers require 16, 32, or 64-byte alignment.
- Cache Optimization: Preventing data form straddling cache lines.
The alignas specifier forces strict alignment.
// Force 32-byte alignment for AVX2 instructions
struct alignas(32) Vector4 \{
float x, y, z, w;
\};
Restriction: alignas can only increase alignment (make it stricter). It cannot decrease it (make it packed).
Over-Alignment and new
Prior to C++17, new Vector4 might crash because the standard heap allocator (malloc) only Guarantees alignment up to max_align_t ( 8 or 16 bytes).
C++17 introduced Aligned New:
- The compiler automatically generates calls to
operator new(size_t, std::align_val_t)when allocating over-aligned types. - The runtime maps this to aligned allocation APIs (e.g.,
_aligned_mallocon Windows,posix_memalignon Linux).
4. Hardware Architecture: Cache Lines and False Sharing
Modern CPUs interact with memory in Cache Lines ( 64 bytes). The CPU cannot load a Single byte; it loads the entire 64-byte line containing that byte.
False Sharing
This architecture creates a concurrency hazard. If two independent variables reside on the same Cache line, and two threads modify them independently, the cores will fight over ownership of the Cache line. This “Ping-Pong” effect stalls the cores.
Scenario:
struct SharedState \{
std::atomic<int> core1_counter; // Thread A writes this
std::atomic<int> core2_counter; // Thread B writes this
\};
Because these two integers likely sit next to each other, they share a cache line.
Destructive Interference Size (C++17)
To prevent this, we must force the variables onto separate cache lines using std::hardware_destructive_interference_size.
#include <new>
#include <atomic>
struct SharedState \{
alignas(std::hardware_destructive_interference_size) std::atomic<int> core1_counter;
alignas(std::hardware_destructive_interference_size) std::atomic<int> core2_counter;
\};
This ensures the struct adds enough padding between members so that they never occupy the same L1 Cache line, regardless of the target CPU architecture.
5. Structure Packing (Breaking Alignment)
In network programming or binary file parsing, formats often ignore alignment rules to save Bandwidth. They are “Packed.”
Standard C++ does not provide a mechanism to pack structures, as it generates code that may crash on Non-x86 architectures. However, compiler extensions are universally supported.
struct __attribute__((packed)) NetworkHeader \{
uint8_t version; // Offset 0
uint32_t id; // Offset 1 (Misaligned!)
\};
#pragma pack(push, 1)
struct NetworkHeader \{
uint8_t version;
uint32_t id;
\};
#pragma pack(pop)
The Cost of Packing
Accessing id in the example above generates different assembly instructions.
- Aligned: A single
MOVinstruction. - Packed (x86): A
MOVinstruction (hardware handles the split load, but with a latency penalty). - Packed (ARMv7/RISC-V): Multiple single-byte loads followed by shifts and ORs, or a hardware trap handled by the kernel (extremely slow).
Best Practice: Do not pass packed structures around your application. Deserialize them Immediately into aligned native structures at the I/O boundary.
6. Bit-Fields
Bit-fields allow packing multiple small integers into a single storage unit. The compiler handles The shift and mask operations transparently. However, the layout of bit-fields is implementation-defined [N4950 §11.4.9]:
#include <iostream>
struct Flags \{
unsigned int readable : 1;
unsigned int writable : 1;
unsigned int executable : 1;
unsigned int reserved : 29;
\};
int main() \{
std::cout << "sizeof(Flags): " << sizeof(Flags) << "\n"; // in standard practice 4
std::cout << "alignof(Flags): " << alignof(Flags) << "\n"; // in standard practice 4
Flags f\{\};
f.readable = 1;
f.writable = 1;
std::cout << "flags value: " << *(unsigned int*)&f << "\n"; // 3 (binary: 11)
\}
Bit-Field Allocation Rules
The C++ standard specifies only that bit-fields are allocated from left to right or right to left (implementation-defined) within an addressable storage unit. The following are implementation- Defined:
- Whether bit-fields are allocated from the high-order or low-order bit.
- Whether a bit-field that does not fit in the remaining space is packed into the next unit or padding is inserted.
- The alignment of the storage unit.
#include <iostream>
struct Example \{
unsigned int a : 10; // 10 bits
unsigned int b : 10; // 10 bits — may or may not straddle the 16-bit boundary
unsigned int c : 10; // 10 bits
unsigned int d : 3; // 3 bits
\};
int main() \{
std::cout << "sizeof(Example): " << sizeof(Example) << "\n";
// On most implementations: 4 bytes (all fit in one unsigned int)
// But this is NOT guaranteed by the standard
\}
Bit-Fields and Portability
Bit-field layout is not portable across compilers or architectures. Never use bit-fields for Wire formats or file formats. Use explicit shift and mask operations instead:
#include <cstdint>
#include <iostream>
// PORTABLE: explicit bit manipulation
struct PortableFlags \{
std::uint32_t raw = 0;
bool readable() const \{ return (raw & 0x1u) != 0; \}
bool writable() const \{ return (raw & 0x2u) != 0; \}
bool executable() const \{ return (raw & 0x4u) != 0; \}
void set_readable(bool v) \{ raw = (raw & ~0x1u) | (v ? 0x1u : 0u); \}
void set_writable(bool v) \{ raw = (raw & ~0x2u) | (v ? 0x2u : 0u); \}
void set_executable(bool v) \{ raw = (raw & ~0x4u) | (v ? 0x4u : 0u); \}
\};
int main() \{
PortableFlags f;
f.set_readable(true);
f.set_executable(true);
std::cout << "raw: " << f.raw << "\n"; // 5
\}
7. Unions and Layout
A union overlays all members at the same memory address. The size of a union is the maximum of its Members’ sizes, rounded up to the alignment of the strictest member [N4950 §11.5]:
#include <iostream>
#include <cstdint>
union FloatBits \{
float f;
std::uint32_t u;
std::uint8_t bytes[4];
\};
int main() \{
std::cout << "sizeof(FloatBits): " << sizeof(FloatBits) << "\n"; // 4
std::cout << "alignof(FloatBits): " << alignof(FloatBits) << "\n"; // 4
FloatBits fb;
fb.f = 1.0f;
std::cout << "float: " << fb.f << "\n";
std::cout << "uint32: 0x" << std::hex << fb.u << std::dec << "\n"; // IEEE 754: 0x3f800000
\}
Type Punning with std::memcpy
#include <iostream>
#include <cstdint>
#include <cstring>
float uint32_to_float(std::uint32_t bits) \{
float result;
std::memcpy(&result, &bits, sizeof(result));
return result;
\}
std::uint32_t float_to_uint32(float f) \{
std::uint32_t result;
std::memcpy(&result, &f, sizeof(result));
return result;
\}
int main() \{
std::cout << std::hex << float_to_uint32(1.0f) << "\n"; // 3f800000
std::cout << uint32_to_float(0x3f800000) << "\n"; // 1
\}
8. Nested Structures and Alignment
When a struct contains another struct as a member, the inner struct’s alignment requirement is Propagated to the outer struct. The inner struct is treated as a single unit with its own alignment:
#include <iostream>
struct Inner \{
double d; // 8 bytes, alignment 8
int i; // 4 bytes, alignment 4
char c; // 1 byte
// 3 bytes padding -> sizeof(Inner) = 16, alignof(Inner) = 8
\};
struct Outer \{
char a; // 1 byte
// 7 bytes padding -> align Inner to 8
Inner inner; // 16 bytes
char b; // 1 byte
// 7 bytes padding -> sizeof(Outer) = 32, alignof(Outer) = 8
\};
int main() \{
std::cout << "sizeof(Inner): " << sizeof(Inner) << "\n"; // 16
std::cout << "alignof(Inner): " << alignof(Inner) << "\n"; // 8
std::cout << "sizeof(Outer): " << sizeof(Outer) << "\n"; // 32
std::cout << "alignof(Outer): " << alignof(Outer) << "\n"; // 8
\}
Array of Structs and Memory Layout
When arrays of structs are used in performance-critical code (e.g., game engines, simulations), the Struct layout directly affects cache utilization. A common optimization is to split hot and cold Fields into separate arrays (Struct of Arrays vs Array of Structs):
#include <iostream>
#include <vector>
#include <cstddef>
// Array of Structs (AoS) — good for per-object access
struct ParticleAoS \{
float x, y, z; // position (hot in physics update)
float vx, vy, vz; // velocity (hot in physics update)
float mass; // hot in physics update
float color[4]; // cold: only used for rendering
std::uint32_t id; // cold: only used for identification
\};
// Struct of Arrays (SoA) — good for batch processing
struct ParticlesSoA \{
std::vector<float> x, y, z;
std::vector<float> vx, vy, vz;
std::vector<float> mass;
// color and id stored separately, loaded only when rendering
\};
int main() \{
std::cout << "sizeof(ParticleAoS): " << sizeof(ParticleAoS) << "\n";
// In standard practice: 4*3 + 4*3 + 4 + 4*4 + 4 = 40 bytes
// With alignment padding, likely 40 or 44 bytes
// When iterating over 1000 particles for physics:
// AoS: loads 40 bytes per particle, but only uses 28 bytes (position + velocity + mass)
// SoA: loads only the fields needed, packed tightly in separate arrays
std::cout << "AoS: " << sizeof(ParticleAoS) << " bytes per particle\n";
\}
Intuition
Alignment is like packing a suitcase — you cannot just throw items in randomly; some items must be placed in specific positions to fit properly. The compiler inserts padding bytes between struct members to ensure each member starts at an address that is a multiple of its alignment requirement. This wastes memory but enables the CPU to read data in a single instruction. Without alignment, the CPU might need two memory reads to fetch a single value that straddles a cache line boundary — like needing to open two doors to walk through one doorway. The alignas and alignof keywords give you explicit control over this packing process.
Common Pitfalls
1. Assuming Layout Across Compilers
Structure layout is implementation-defined. A struct compiled with GCC on Linux may have different Padding than the same struct compiled with MSVC on Windows. Never serialize raw struct memory to Disk or over the network. Always use explicit serialization:
#include <cstdint>
#include <vector>
#include <cstring>
struct NetworkMessage \{
std::uint32_t type;
std::uint64_t timestamp;
std::uint16_t length;
\};
// PORTABLE serialization
std::vector<std::uint8_t> serialize(const NetworkMessage& msg) \{
std::vector<std::uint8_t> buf(sizeof(msg));
std::memcpy(buf.data(), &msg, sizeof(msg));
// For cross-platform, serialize each field individually with known byte order
return buf;
\}
2. [[no_unique_address]] and EBO Limits
[[no_unique_address]] relies on Empty Base Optimization (EBO). It only saves space when the empty Member can overlap with padding bytes. If the struct has no padding, no space is saved:
#include <iostream>
struct Empty \{\};
struct NoSavings \{
Empty e; // 1 byte — no padding to overlap with
// sizeof(NoSavings) == 1
\};
struct WithPadding \{
int i; // 4 bytes
[[no_unique_address]] Empty e; // overlaps with padding after i
// sizeof(WithPadding) == 4
\};
int main() \{
std::cout << "sizeof(Empty): " << sizeof(Empty) << "\n"; // 1
std::cout << "sizeof(NoSavings): " << sizeof(NoSavings) << "\n"; // 1
std::cout << "sizeof(WithPadding): " << sizeof(WithPadding) << "\n"; // 4
\}
3. Alignment and reinterpret_cast
Casting a char* to an int* at an arbitrary address is undefined behavior if the address is not Properly aligned for int. This is a common mistake when parsing binary data:
#include <cstdint>
#include <cstring>
#include <iostream>
// WRONG: buffer may not be aligned for uint32_t
void parse_wrong(const char* buffer) \{
// auto value = *reinterpret_cast<const std::uint32_t*>(buffer); // UB if misaligned
\}
// CORRECT: use memcpy (compiler optimizes to a single load on x86)
void parse_correct(const char* buffer) \{
std::uint32_t value;
std::memcpy(&value, buffer, sizeof(value));
std::cout << "value: " << value << "\n";
\}
4. std::hardware_destructive_interference_size is Non-Constant
std::hardware_destructive_interference_size is 64 on x86 but may differ on other Architectures (e.g., 128 on some ARM implementations). It is a const value determined at runtime On some implementations, not a compile-time constant. This means it cannot be used as a template Argument or in constexpr contexts on all implementations:
#include <iostream>
#include <new>
int main() \{
// This is in standard practice 64, but not guaranteed at compile time
std::cout << "destructive_interference_size: "
<< std::hardware_destructive_interference_size << "\n";
std::cout << "constructive_interference_size: "
<< std::hardware_constructive_interference_size << "\n";
\}
Summary
This topic covers the essential concepts and techniques related to alignment and layout, including key principles and practical applications.
Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis
A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.
Worked Examples
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.