Post

Rust Internals: Memory Usage and Efficiency

How Type Alignment,Layout and trait objects affect Rusts Memory Usage and Efficiency

As software engineers, we are always striving to write efficient,performant and cost effective applications.

In Rust, understanding type alignment, layouts, and trait objects is crucial for optimizing memory usage and minimizing overhead.

In this blog post, we’ll delve into how representation types such as

  • repr(c),
  • repr(Rust),
  • repr(aligned),
  • repr(packed), and
  • repr(transparent) play a significant role in achieving efficiency and performance in Rust applications.

Type Alignment and Layouts

All values must be byte aligned. They must be placed at an address that is a multiple of 8 bits

Type alignment refers to how data is arranged in memory, particularly with respect to the alignment of individual fields within a struct or object.

“Operations on data that is not aligned are referred to as misaligned accesses and can lead to poor performance and bad concurrency problems.”

Rust allows developers to control type alignment and memory layout using the repr attribute.

The Compiler tries to take advantage of aligned values, since they are generally faster to access provide stronger consistency semantics.

Let’s explore the different representation types and their implications:

repr(c)

This representation orders fields exactly as they are defined, similar to how they would be laid out in C. It ensures compatibility with C code and interoperability with external libraries.

However, it may introduce padding between fields for alignment purposes, potentially increasing memory usage.

They are essential for writing rust code that interfaces with other languages through the Foreign Function Interface.

The figure below illustrates how the compiler lays out a struct with repr(c).

It adds padding-bytes in order to make the struct type, byte-aligned.

Also notice that, the total number of bytes (12) is a multiple of the largest type [b:u32] in the ReprC struct.

repr(c) reprc

repr(Rust)

By default, Rust optimizes memory layout for performance. It may reorder fields within a struct to minimize memory usage and improve cache locality.

This representation is suitable for most Rust code where interoperability with C is not a concern.

The figure below illustrates how the compiler lays our the memory of the same struct type with repr(rust).

It uses no padding. But if we count the bytes, they add up to 7. 7 isn’t byte aligned for type ReprRust. For ReprRust to be byte aligned, the compiler adds 1 byte to 7 to make it 8 bytes in total, which is a multiple of the largest type in the struct.

repr(c) reprc

repr(aligned)

This representation allows developers to specify a particular alignment for a struct. It adds padding between fields to ensure that each field starts at a memory address that is a multiple of the specified alignment.

While this can improve performance in certain scenarios, it may increase memory usage.

In the figure below, notice how a lot of padding has been added to make the types aligned to 16 bytes.

repr(c) reprc

repr(packed)

The packed representation removes all padding between fields, packing them as closely together as possible.

This can save memory but may result in inefficient memory access due to misaligned data accesses, especially on architectures that require aligned memory access.

repr(c) reprc

repr(transparent)

This representation allows a struct to have the same memory layout as another struct it wraps, effectively inheriting its memory layout.

It is useful for creating new types with the same memory representation as existing types, without introducing additional overhead.

1
2
#[repr(transparent)]
struct ReprTransparent(ReprC);

Or

1
2
#[repr(transparent)]
struct ReprTransparent(ReprRust);

Here’s a small code illustration of everything we’ve discussed above. Please run the code and see how much memory each struct consumes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#![allow(dead_code)]
#[repr(C)]
struct ReprC {
    a: u8,
    b: u32,
    c: u16,
}

#[repr(Rust)]
struct ReprRust {
    a: u8,
    b: u32,
    c: u16,
}

#[repr(align(16))]
struct ReprAligned {
    a: u8,
    b: u32,
    c: u16,
}

#[repr(packed)]
struct ReprPacked {
    a: u8,
    b: u32,
    c: u16,
}

#[repr(transparent)]
struct ReprTransparent(ReprC);

fn main() {
    println!("Size of ReprC: {}", std::mem::size_of::<ReprC>());
    println!("Size of ReprRust: {}", std::mem::size_of::<ReprRust>());
    println!("Size of ReprAligned: {}", std::mem::size_of::<ReprAligned>());
    println!("Size of ReprPacked: {}", std::mem::size_of::<ReprPacked>());
    println!("Size of ReprTransparent: {}", std::mem::size_of::<ReprTransparent>());
}



1
2
3
4
5
6
 Size of ReprC: 12
 Size of ReprRust: 8
 Size of ReprAligned: 16
 Size of ReprPacked: 7
 Size of ReprTransparent: 12

Trait Objects and Performance

Trait objects in Rust allow for dynamic dispatch, enabling polymorphic behavior without sacrificing performance. However, they come with some overhead due to vtable lookup and potential heap allocation.

When working with trait objects, it’s essential to consider the impact on performance:

  • Dynamic Dispatch Overhead: Trait objects incur a small performance overhead compared to static dispatch due to the need for vtable lookup at runtime. This overhead is usually negligible but can become significant in performance-critical sections of code.

  • Heap Allocation: Trait objects are typically stored on the heap, requiring additional memory allocation and deallocation overhead. This can impact cache efficiency and increase memory fragmentation, especially in tight memory environments.

  • Trait Object Size: The size of a trait object is determined by the size of the vtable pointer plus any additional data in the object. This overhead should be considered when designing APIs and data structures that utilize trait objects.

Conclusion

In Rust, understanding type alignment, layouts, and trait objects is essential for writing efficient and performant code. By choosing the appropriate representation types (repr) and minimizing the use of trait objects where possible, developers can optimize memory usage, minimize overhead, and maximize performance.

While Rust’s default memory layout optimizations make it well-suited for most use cases, developers should carefully consider the performance implications when working with trait objects and choose the representation types that best suit their specific requirements.

By leveraging Rust’s powerful features and understanding how they impact efficiency and performance, developers can build high-performance applications that are both robust and maintainable.

REFERENCE

Rust for Rustaceans by Jon Gjengset

This post is licensed under CC BY 4.0 by the author.