Introduction
Memory alignment refers to the way data is arranged and accessed in computer memory. Data types have an intrinsic alignment requirement, which ensures efficient access and correct behaviour on a given architecture.
- Aligned Memory Access: Accessing memory on its alignment boundary often results in faster operations.
- Misaligned Memory Access: Can lead to performance penalties or even crashes, especially with some processors and SIMD instruction sets.
The C++11 standard introduced the alignof operator, which can be used to query the alignment requirement of a type:
std::cout << alignof(double) << std::endl; // output 8
Basic Data Types
For many basic data types:
- The alignment requirement is often the same as the size.
- An
intmight be 4 bytes in size with a 4-byte alignment requirement. - A
doublemight be 8 bytes in size with an 8-byte alignment requirement.
Vector Types
Typically, vector types in C++ behave like any other data type. They can be passed as parameters, returned from functions, and used in arrays. The compiler handles these gracefully for static memory allocation.
Example:
float8_t example(float8_t a, float8_t b) {
float8_t c[2];
c[0] = a + b;
c[1] = a - b;
float8_t d = c[0] * c[1];
return d;
}
However, pointers to vector data need to point to memory blocks that are aligned, which means the Memory address must be a multiple of 321 (bytes). If the alignment requirement are not satisifed, the programme may crash.
Example of broken code:
void crash() {
float8_t a = {1,2,3,4,5,6,7,8};
float8_t* p = (float8_t*)malloc(sizeof(float8_t));
float8_t* q = (float8_t*)malloc(sizeof(float8_t));
*p = a;
*q = a;
a = *p + *q;
free(p);
free(q);
}
This may cause a segmentation fault2. To avoid such error, we can ensure memory allocation alignment with following methods,
- From
C++17onwards, use standard containers with vector types3:std::vector<float8_t> x(10); - For older compilers, consider
posix_memalignoraligned_alloc4 from the C11 and C++17 libraries.
Classes and Structs
Classes and structs derive their alignment requirements from their members:
- Alignment Requirement of a
Class/Struct: Maximum alignment requirement of its members. - Size: Typically a multiple of its alignment requirement.
- Padding: Compiler might insert padding to ensure members’ proper alignment.
class Example {
char a; // 1 byte
int b; // 4 bytes, with a typical 4-byte alignment
char c; // 1 byte
};
To ensure b is aligned, padding might be added after a and potentially after c.
While padding ensures correct and optimal memory accesses, there are ways to control or eliminate it. For instance, many compilers offer directives to control padding:
#pragma pack(push, 1)
class PackedExample {
char a;
int b;
char c;
};
#pragma pack(pop)
In addition, reordering members based on alignment can reduce padding:
class OptimizedExample {
int b;
char a;
char c;
};
Finally, we can use bit-fields, to achieve compact storage for flags or small integers.
A bit-field is defined inside a structure (or union) and allows you to specify a fixed number of bits for an integer type. The syntax is as follows:
struct_type member_name : number_of_bits;
Here’s a basic example:
#include <iostream>
struct BitFieldExample {
unsigned int flag1 : 1; // Uses only 1 bit
unsigned int flag2 : 1; // Uses another bit
unsigned int value : 6; // Uses 6 bits
};
int main() {
BitFieldExample example;
// Setting the values
example.flag1 = 1; // Setting flag1 to true
example.flag2 = 0; // Setting flag2 to false
example.value = 42; // Setting value to 42, which is within the 6-bit range [0, 63]
// Accessing the values
std::cout << "Flag1: " << example.flag1 << std::endl;
std::cout << "Flag2: " << example.flag2 << std::endl;
std::cout << "Value: " << example.value << std::endl;
// Toggle the flags
example.flag1 = !example.flag1; // Setting flag1 to false (toggle)
example.flag2 = !example.flag2; // Setting flag2 to true (toggle)
// Increment the value
if (example.value < 63) {
example.value++; // Increment value by 1
}
return 0;
}
Bit-fields are often used in scenarios like:
- Protocol Headers: When working with low-level networking, bit-fields can be used to represent protocol header structures.
- Hardware Register Representations: When doing low-level hardware programming or embedded programming, hardware registers often have specific bits set aside for specific flags or values.
- Compact Data Structures: In scenarios where memory is a constraint and you want a compact representation of your data.
-
SIMD architectures such as SSE and AVX on x86 processors have specific alignment requirements. SSE: Requires 16-byte alignment for its 128-bit registers. AVX: Requires 32-byte alignment for its 256-bit registers. AVX-512: Requires 64-byte alignment for its 512-bit registers. If the vector type
float8_tin the example uses AVX instructions (which handle 8 single-precision floating-point (fp32) numbers simultaneously, thus occupying 256 bits or 32 bytes), then it would require 32-byte alignment. ↩ -
The line
*p = a;might be attempting to use SIMD instructions to store thefloat8_tvalueainto the memory pointed to byp. Ifpisn’t 32-byte aligned, this can result in a crash. ↩ -
Prefer
std::vectorwhen possible. ↩ -
If using these methods, it’s crucial to free memory correctly to prevent errors. ↩