🎓 A1Learner
🔍

Rust Type Sizes

A type's size is the number of bytes one value of it takes. The size of a struct or enum decides how much memory it uses and how fast it copies.

Measure any type

size_of answers in bytes:

println!("u8 = {}, u32 = {}, &u8 = {}", size_of::<u8>(), size_of::<u32>(), size_of::<&u8>());

Padding between fields

A u32 must start at a multiple of 4, so a byte before it leaves 3 wasted bytes. Pin the field order with #[repr(C)] to see it:

#[repr(C)]
struct Mixed { a: u8, b: u32, c: u8 }
println!("repr(C) mixed order = {}", size_of::<Mixed>());

Box the big variant

This enum is 101 bytes because the tag has to fit beside the big variant. A box moves the bulk to the heap:

enum Big { Small(u8), Large([u8; 100]) }
enum BigBoxed { Small(u8), Large(Box<[u8; 100]>) }
println!("plain = {}, boxed = {}", size_of::<Big>(), size_of::<BigBoxed>());

The tag that costs nothing

A Box is 8 bytes. Option<Box<u32>> is also 8. The tag hides in the address 0. A Box can never hold that address:

println!("Option<Box<u32>> = {}", size_of::<Option<Box<u32>>>());

size_of_val weighs one value

size_of names a type. size_of_val measures the value you hand it. A &str value is the text itself, so it weighs 6 here. The handle that points at the text weighs 16:

let text: &str = "ferris";
println!("the text = {}", std::mem::size_of_val(text));
println!("the handle = {}", std::mem::size_of_val(&text));

Example

use std::mem::size_of;
fn main() {
    println!("bool = {}", size_of::<bool>());
    println!("u8 = {}", size_of::<u8>());
    println!("u32 = {}", size_of::<u32>());
    println!("char = {}", size_of::<char>());
    println!("&u8 = {}", size_of::<&u8>());
    println!("Box<u8> = {}", size_of::<Box<u8>>());
    println!("Vec<u8> = {}", size_of::<Vec<u8>>());
    println!("String = {}", size_of::<String>());
    println!("Option<u32> = {}", size_of::<Option<u32>>());
    println!("Option<&u8> = {}", size_of::<Option<&u8>>());
}

Key points

  • Use std::mem::size_of::<T>() to measure a type in bytes
  • size_of_val measures one value instead of a type
  • Alignment: a type must start at an address that is a multiple of its alignment
  • A u32 aligns to 4
  • A u64 aligns to 8
  • Ordinary Rust structs get their fields reordered by the compiler to waste less space
  • #[repr(C)] pins the written order, padding included
  • An enum costs the size of its biggest variant, plus room for the tag that says which variant it is
  • usize and isize are pointer-sized: 8 bytes on a 64-bit machine, so lengths and indexes cost one word each
  • A u8 fits in one byte and a u32 needs four.
  • Every value takes a fixed number of bytes, and that number is its type's size.

In this example, the sizes of Rust's building blocks are printed on a 64-bit machine. A char is 4 bytes because it is a full Unicode scalar value. A reference and a Box are both one pointer — 8 bytes. A Vec and a String are three words (pointer, length, capacity): 24 bytes. And the niches show their power twice. Option<u32> needs 4 extra bytes for the tag. Option<&u8> costs no extra at all.

Note

Sizes here are for 64-bit computers, the common case today

On a 32-bit machine every pointer-sized value — &, Box, Vec, String — is 4 bytes instead of 8

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

An enum has variants holding a u8 and a [u8; 100]. Roughly how big is it, and what shrinks it?