All articles
Rust Byte Alignment Basics
Performance & Optimization

Rust Byte Alignment Basics

Byte alignment, also known as data alignment, refers to arranging the memory addresses of data structures so that they align with certain…

By Luis SoaresJanuary 26, 2024Original on Medium

Byte alignment, also known as data alignment, refers to arranging the memory addresses of data structures so that they align with certain byte boundaries.

This alignment is crucial for performance reasons, as most hardware is designed to read or write data efficiently at aligned addresses.

An aligned memory address is typically a multiple of the word size of the processor (where the word size is commonly 4 or 8 bytes on most modern architectures).

Why is Byte Alignment Important?

Aligned data accesses are faster than unaligned accesses because they do not require additional cycles to fetch parts of the data from multiple words. Moreover, some architectures do not support unaligned accesses at all, leading to hardware faults. In Rust, respecting byte alignment is critical for avoiding undefined behavior and ensuring that operations on data are as efficient as possible.

How Rust Handles Byte Alignment

Rust, being a systems programming language, provides control over byte alignment through its type system and attributes. The compiler automatically aligns most types to their natural boundaries for efficient access. However, when dealing with FFI (Foreign Function Interface) or low-level memory operations, you might need to manually specify alignments.

Default Alignment

By default, Rust aligns data types to their “natural” alignment, which is usually the size of the largest field for structs or the size of the type itself for primitives. Let’s look at an example:

struct MyStruct {
    a: u32,
    b: u8,
}

fn main() {
    println!("Size of MyStruct: {}", std::mem::size_of::<MyStruct>());
    println!("Alignment of MyStruct: {}", std::mem::align_of::<MyStruct>());
}

In this example, MyStruct contains a u32 and a u8. The largest field (u32) has a size of 4 bytes, so the entire struct will be aligned to a 4-byte boundary.

Custom Alignment

For cases where you need a specific alignment, perhaps to match the memory layout of C structures or to optimize cache usage, Rust provides the #[repr(align(N))] attribute. Here's how you can use it:

#[repr(align(8))]
struct AlignedStruct {
    a: u32,
    b: u8,
}

fn main() {
    println!("Size of AlignedStruct: {}", std::mem::size_of::<AlignedStruct>());
    println!("Alignment of AlignedStruct: {}", std::mem::align_of::<AlignedStruct>());
}

In this code, AlignedStruct is explicitly aligned to an 8-byte boundary, regardless of the natural alignment of its fields. This is useful when interfacing with other languages or hardware that expects data at specific alignments.

Padding and Memory Layout

Rust introduces padding to satisfy alignment requirements, which can affect the memory layout of structures. Consider the following example:

struct PaddedStruct {
    a: u8,
    // Padding of 3 bytes here to align `b` on a 4-byte boundary
    b: u32,
}

fn main() {
    println!("Size of PaddedStruct: {}", std::mem::size_of::<PaddedStruct>());
}

Although a is only 1 byte and b is 4 bytes, the size of PaddedStruct will be 8 bytes due to padding added to align b on a 4-byte boundary.

Practical Implications

Understanding and managing byte alignment is crucial for systems programming, especially for performance-critical applications. Properly aligned data ensures that your Rust programs can run efficiently and interface seamlessly with other languages and hardware. When dealing with FFI, always ensure that your Rust structures have compatible alignments with the corresponding structures in the foreign language to prevent undefined behavior and potential crashes.

Continuing from where we left off, let’s delve deeper into more advanced aspects of byte alignment in Rust, including the alignment of arrays and enums, and explore how to inspect and manipulate memory layouts for optimization and interoperability purposes.

Alignment of Arrays

In Rust, arrays are a sequence of elements of the same type. The alignment of an array is determined by the alignment of its element type. This ensures that each element of the array is properly aligned. Consider an array of u16 values:

fn main() {
    println!("Alignment of [u16; 3]: {}", std::mem::align_of::<[u16; 3]>());
}

Since u16 has an alignment of 2 bytes, the entire array will also have an alignment of 2 bytes, ensuring that each u16 element within the array is aligned on a 2-byte boundary.

Alignment of Enums

Enums in Rust can have different variants with different types and sizes. Rust aligns enums based on the variant with the strictest alignment requirement, ensuring that any variant of the enum is correctly aligned. Here’s an example:

enum MyEnum {
    A(u32),
    B(u64),
}

Practice what you learned

Reinforce this article with hands-on coding exercises and AI-powered feedback.

View all exercises

fn main() { println!("Alignment of MyEnum: {}", std::mem::align_of::<MyEnum>()); }


In this case, `MyEnum` will have an alignment of 8 bytes (the alignment of `u64`), which is the strictest alignment requirement among its variants.

### Inspecting and Manipulating Memory Layouts

Rust provides several functions in the `std::mem` module to inspect and manipulate memory layouts, such as `size_of`, `align_of`, and `size_of_val`. These functions are invaluable for understanding how Rust lays out data in memory and for ensuring that your data structures are optimized for both space and access speed.

For instance, you might use these functions to dynamically calculate the size and alignment of data structures when working with raw pointers or performing dynamic memory allocations.

### Alignment for Performance Optimization

Proper alignment can significantly impact the performance of your Rust programs. Misaligned data can lead to cache line splits, where a single piece of data spans across two cache lines, requiring two cache accesses instead of one. Ensuring that frequently accessed structures are aligned to cache line boundaries (commonly 64 bytes on modern architectures) can lead to substantial performance improvements, especially in concurrent or high-throughput scenarios.

Here’s an example of aligning a structure to a 64-byte cache line:

```rust
#[repr(align(64))]
struct CacheOptimizedStruct {
    data: [u64; 8], // 64 bytes in total
}

fn main() {
    println!("Alignment of CacheOptimizedStruct: {}", std::mem::align_of::<CacheOptimizedStruct>());
}

Interoperability with C

When interfacing with C libraries, ensuring that your Rust structures have the same memory layout and alignment as their C counterparts is crucial. The #[repr(C)] attribute can be combined with #[repr(align(N))] to both match the C memory layout and specify a particular alignment:

#[repr(C, align(4))]
struct CInteropStruct {
    a: u32,
    b: u16,
}

fn main() {
    // Use this struct in FFI calls
}

This struct is not only aligned to a 4-byte boundary but also guaranteed to have the same memory layout as a similar struct defined in a C program, making it safe for FFI.

Demonstrating Performance Impacts with a simple example

  1. Setup: We’ll need a Rust environment. Ensure you have Rust and Cargo installed. If not, you can install them from the official Rust website.
  2. Project Creation: Create a new Rust project using Cargo:
cargo new rust_alignment_demo cd rust_alignment_demo

3. Writing the Application: Open the src/main.rs file and replace its contents with the following code:

use std::time::Instant;

#[derive(Clone, Copy)]
struct AlignedStruct {
    a: u64, // Naturally aligned
    b: u64,
}

#[repr(C, packed)]
#[derive(Clone, Copy)]
struct MisalignedStruct {
    a: u8,  // This will cause misalignment for `b`
    b: u64, // Misaligned
}

fn main() {
    let iterations = 100_000_000;

    let aligned_struct = AlignedStruct { a: 1, b: 2 };
    let misaligned_struct = MisalignedStruct { a: 1, b: 2 };

    // Benchmarking aligned access
    let start_aligned = Instant::now();
    for _ in 0..iterations {
        let _ = volatile_access(&aligned_struct);
    }
    let elapsed_aligned = start_aligned.elapsed();

    // Benchmarking misaligned access
    let start_misaligned = Instant::now();
    for _ in 0..iterations {
        let _ = volatile_access(&misaligned_struct);
    }
    let elapsed_misaligned = start_misaligned.elapsed();

    println!("Aligned access:    {:?}", elapsed_aligned);
    println!("Misaligned access: {:?}", elapsed_misaligned);
}

/// A function that performs a read using volatile access to prevent the compiler from optimizing away the access.
/// Works for both aligned and misaligned structs due to the use of references.
fn volatile_access<T>(data: &T) -> u64 {
    unsafe { std::ptr::read_volatile(data as *const T as *const u64) }
}
  • We define two structs: AlignedStruct is naturally aligned as both its fields are u64, ensuring 8-byte alignment. MisalignedStruct uses #[repr(C, packed)] to avoid natural padding, causing b to be misaligned following the u8 field a.
  • The main function initializes instances of these structs and benchmarks them by performing a large number of read operations. The volatile_access function ensures that the compiler does not optimize away the reads.
  • We use std::time::Instant to measure the time taken for the operations on both structs.

Build and run the application using Cargo:

cargo run --release

Running in release mode is crucial for benchmarks to disable debug checks and optimizations that could affect timing.

The application will print the time taken for aligned and misaligned accesses. Typically, aligned access should be faster, but the actual difference can vary based on your system’s architecture and current workload.

Practice what you learned

Reinforce this article with hands-on coding exercises and AI-powered feedback.

View all exercises

Want to practice Rust hands-on?

Go beyond reading — solve interactive exercises with AI-powered code review on Rust Lab.