Full title: [Advanced Rust] 1.14. Memory Types Pt.2 - Dynamically Sized Types and Wide Pointers, Packed Layouts, Larger Alignment for Specific Fields or Types, Memory Representation of Complex Types, and Repr Rust

1.14.1. repr(Rust)

Remember the example in the previous article? That example used repr(C), and the limitation of the C representation is that all fields must be placed in the same order as they are defined in the original struct.

repr(Rust) is the default representation. It intentionally provides fewer layout guarantees than repr(C): the compiler may reorder fields, and two types with the same fields in the same order are still not guaranteed to share a layout.

Because the compiler may reorder fields (for example, placing larger fields first), padding can often be reduced. In the Foo example from the previous article, one possible optimized layout needs no padding.

With fewer guarantees about layout, the compiler has room to rearrange things and produce efficient code.

If repr(Rust) is used, then one possible memory layout of the Foo struct from above is:

Code Field Type Size Default Representation Padding Final Alignment
#[repr(Rust)]
struct Foo {
long: u64, 8 bytes 8-byte aligned 8 bytes
normal: u32, 4 bytes 4-byte aligned
short: u16, 2 bytes 2-byte aligned
small: u8, 1 byte 1-byte aligned
tiny: bool, 1 byte 1-byte aligned
}
Total 16 bytes
  • The compiler first orders the fields by size, putting the largest first so that it can determine what alignment the struct should use. In this example, u64 is the largest and takes 8 bytes, so the struct is aligned to 8 bytes
  • The compiler then looks at the remaining fields and sees that their total size is exactly 8 bytes, so it can place them together and avoid padding
  • In the end, this struct only needs 16 bytes, which saves half the memory compared with repr(C)
  • This is more efficient, but compilation time may be a little longer

1.14.2. Packed Layouts

You can tell the compiler that no padding is needed between fields, but then you must accept the performance cost of misaligned access.

When memory is limited or when there are many instances of a type, a packed layout can be useful. It is also useful when sending a memory representation over a low-bandwidth network connection.

To enable a packed layout, add the #[repr(packed)] annotation to the type.

Note that with a packed layout:

  • Code may run more slowly
  • In extreme cases, if the CPU supports only aligned access, the program may crash

1.14.3. Giving a Specific Field or Type a Larger Alignment

Using the #[repr(align(n))] annotation lets you give a specific field or type a larger alignment, where n is the argument.

For example, if you want to ensure that different values stored contiguously in memory (like in an array) end up on different CPU cache lines, you can avoid false sharing.

Here is a brief explanation of the related terms:

  • Cache is composed of cache lines, and caches operate on cache lines as units. A cache line is the smallest data unit that can be mapped into the cache
  • False sharing happens when two different CPUs access different variables that share the same cache line. In theory they could operate in parallel, but in the end they are both competing to update the same cache entry. This can cause a huge performance drop in concurrent programs

1.14.4. Memory Representation of Complex Types

  • Tuples: Their memory representation is like a struct; the field types and tuple element types are in the same order
  • Arrays: A contiguous sequence of the contained type, with no padding between elements
  • Unions: For each field, the layout choice is independent; the alignment is the maximum among all fields
  • Enums: Like unions, but with an additional hidden shared field used to store the discriminant of the enum variant. The code uses the discriminant value to determine which variant a given value contains. The size of the discriminant depends on the number of variants

1.14.5. Dynamically Sized Types and Wide Pointers

Most types in Rust automatically implement the Sized trait.

Rust needs to know some details about its types, such as how much space to allocate for a value of a specific type. That is what makes the concept of dynamically sized types a little confusing. They are sometimes called DSTs or unsized types, and they let us write code that works with values whose size is only known at run time.

To use dynamically sized types, Rust provides the Sized trait to indicate whether the size of a type is known at compile time. Everything whose size is known at compile time automatically implements this trait. Rust also implicitly adds the Sized trait to every generic function. By default, generic functions apply only to types whose size is known at compile time. This restriction can be relaxed with ?Sized. ?Sized means “T may or may not implement Sized,” that is, T may or may not be a dynamically sized type. This notation does not require the default condition that generic types must have a known size at compile time. The ?Trait syntax with this meaning applies only to the Sized trait and no other trait.

What should we do when a function needs to accept a DST — such as a trait object or a slice — as a parameter? We can use a wide pointer (also called a fat pointer).

1.14.6. Wide Pointers

By placing a non-Sized type behind a wide pointer, we bridge the gap between Sized and non-Sized types.

So what exactly is a wide pointer? A wide pointer is an ordinary pointer with an additional "word-size" field attached. It provides the compiler with the extra information it needs about the pointer so that it can generate sensible code that uses that pointer.

When you reference a DST, the compiler automatically constructs a wide pointer for you. For example, the extra information for a slice is the slice’s length.

Wide pointers are Sized because they are pointers at heart, and their size is fixed (twice the size of a “sized” pointer — one field stores the pointer itself, and the other stores the attached metadata used to “complete” the type).

Note: Box<T> and Arc<T> both support storing wide pointers, so they both support ?Sized.