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.

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:

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,

  1. From C++17 onwards, use standard containers with vector types3: std::vector<float8_t> x(10);
  2. For older compilers, consider posix_memalign or aligned_alloc4 from the C11 and C++17 libraries.

Classes and Structs

Classes and structs derive their alignment requirements from their members:

  1. Alignment Requirement of a Class/Struct: Maximum alignment requirement of its members.
  2. Size: Typically a multiple of its alignment requirement.
  3. 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:

  1. 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_t in 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. 

  2. The line *p = a; might be attempting to use SIMD instructions to store the float8_t value a into the memory pointed to by p. If p isn’t 32-byte aligned, this can result in a crash. 

  3. Prefer std::vector when possible. 

  4. If using these methods, it’s crucial to free memory correctly to prevent errors.