Primitive Types, Compound Types & Representation Mechanics

Explore scalar types, hardware alignment formulas, struct padding, slices, and fat pointer representation in memory.

01. Foundations & Type System Mechanics7 min read

Every computational value in Rust corresponds to a concrete, bit-level memory layout. Unlike dynamically typed languages where numbers are wrapped in runtime boxed objects, Rust scalar primitives represent bare hardware representations with deterministic bit widths and hardware alignment constraints.

Fat Pointer Size on 64-bitLayout
16 Bytes

Slice references consist of an 8-byte base address and an 8-byte length indicator.

1. Scalar Primitives#

Scalar primitives represent single values stored directly on the stack or in CPU registers:

CategoryTypesMemory WidthRepresentation
Signed Integersi8, i16, i32, i64, i1281,2,4,8,161, 2, 4, 8, 16 bytesTwo's complement representation.
Unsigned Integersu8, u16, u32, u64, u1281,2,4,8,161, 2, 4, 8, 16 bytesDirect binary magnitude.
Pointer Sizedisize, usize4 or 8 bytes (Target arch)Sized to match platform address space.
Floating Pointf32, f644 or 8 bytesStandard IEEE 754 precision format.
Booleanbool1 byte0x00 (false) or 0x01 (true).
Characterchar4 bytesUnicode Scalar Value (valid UTF-32 range).
Integer Overflow Semantics
In debug mode (cargo run), integer overflow triggers an explicit runtime panic assertion. In release mode (cargo build --release), rustc compiles arithmetic with two's complement wrapping to maximize CPU pipeline throughput. Use explicit checked methods like .checked_add() or .saturating_add() when overflow is anticipated.

2. Compound Types: Tuples & Arrays#

Compound types bundle multiple values together into a single composite type:

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::mem::{size_of, align_of}; fn main() { // 1. Fixed-size Array (homogeneous sequence, stack allocated) let numbers: [i32; 4] = [10, 20, 30, 40]; // 2. Heterogeneous Tuple (multiple types grouped) let record: (u8, u64, u16) = (1, 1000, 42); println!("Array size: {} bytes", size_of::<[i32; 4]>()); // 16 bytes println!("Tuple size: {} bytes", size_of::<(u8, u64, u16)>()); // 16 bytes due to padding! println!("Tuple alignment: {} bytes", align_of::<(u8, u64, u16)>()); // 8 bytes }

3. Mathematical Foundations: Alignment & Padding#

Every type in Rust has an alignment requirement A(T)A(T), which is a power of two (1,2,4,8,…1, 2, 4, 8, \dots). The CPU hardware mandates that an instance of type TT must be stored at a memory address that is a multiple of A(T)A(T).

For a composite tuple T=(T1,T2,…,Tn)T = (T_1, T_2, \dots, T_n), the aggregate alignment requirement is determined by the maximum alignment of its constituent components:

A(T)=max⁡(A(T1),A(T2),…,A(Tn))A(T) = \max\bigl(A(T_1), A(T_2), \dots, A(T_n)\bigr)

The total footprint in bytes S(T)S(T) equals the sum of member sizes plus padding bytes inserted to satisfy individual offsets OiO_i:

S(T)=∑i=1nS(Ti)+∑i=1nPiwhere Oi≡0(modA(Ti))S(T) = \sum_{i=1}^{n} S(T_i) + \sum_{i=1}^{n} P_i \quad \text{where } O_i \equiv 0 \pmod{A(T_i)}

For a fixed array of size NN with elements of type EE:

S(Array(E,N))=N×S(E),A(Array(E,N))=A(E)S(\text{Array}(E, N)) = N \times S(E), \qquad A(\text{Array}(E, N)) = A(E)
Architecture Diagram
Rendering diagram...
Tail Padding
Why are 6 tail padding bytes added at the end? If this tuple is placed in an array, the subsequent element must also begin on an 8-byte aligned memory address.

4. Slice Types & Fat Pointers#

Arrays have fixed lengths known at compile time. Slices, denoted as [T], represent dynamically sized views into contiguous sequences. Because the length is not fixed at compile time, a slice reference &[T] is stored as a fat pointer.

On a 64-bit architecture, a fat pointer requires 16 bytes (two 64-bit words):

  1. Pointer Word (8 bytes): The 64-bit starting address of the backing memory buffer.
  2. Length Word (8 bytes): The number of elements contained in the slice view.
Architecture Diagram
Rendering diagram...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn print_subsequence(slice: &[i32]) { println!("Length: {}", slice.len()); println!("Base pointer: {:p}", slice.as_ptr()); for item in slice { print!("{} ", item); } println!(); } fn main() { let full_array = [100, 200, 300, 400, 500]; // Borrow a view of elements at index 1 to 3 let view: &[i32] = &full_array[1..4]; print_subsequence(view); }

5. Concept Verification#

Verify your grasp of Rust scalar memory representation and fat pointers:

Concept check

Why does a slice reference &[T] require 16 bytes of memory on a 64-bit platform, whereas a normal reference &T requires 8 bytes?

Concept check

How does Rust handle integer overflow when running binaries compiled in release mode?