Padding
Padding consists of unused, uninitialized bytes inserted by the compiler between fields (internal padding) or at the end of a composite type (trailing padding) to satisfy field alignment requirements and ensure the total type size is a multiple of the type's alignment.
Calculating Internal and Trailing Padding
Under sequential layout (#[repr(C)]), the compiler places fields using two rules:
- Field placement: For each field $i$ with alignment $A_i$, if the current offset $O$ is not divisible by $A_i$, insert $(A_i - (O \bmod A_i)) \bmod A_i$ bytes of internal padding so the field starts at an aligned offset.
- Struct completion: After placing the final field, compute the struct alignment $A_{\text{max}} = \max(A_i)$. If the current total offset $O$ is not divisible by $A_{\text{max}}$, append $(A_{\text{max}} - (O \bmod A_{\text{max}})) \bmod A_{\text{max}}$ bytes of trailing padding.
#[repr(C)]
struct Padded {
flag: u8, // size 1, align 1 -> offset 0
// 7 bytes internal padding (offsets 1..=7) so `count` aligns to 8
count: u64, // size 8, align 8 -> offset 8..=15
code: u16, // size 2, align 2 -> offset 16..=17
// 6 bytes trailing padding (offsets 18..=23) so total size (24) % 8 == 0
}
Byte Map (24 bytes total, 13 bytes padding):
+------+--------------------+---------------+--------+--------------------+
| flag | pad (7 bytes) | count (8 B) |code(2B)| pad (6 bytes) |
+------+--------------------+---------------+--------+--------------------+
0 1..7 8..15 16..17 18..23
Ordering the fields in descending alignment order (u64, u16, u8) reduces the total padding in #[repr(C)] from 13 bytes to 5 bytes (16 bytes total).
use std::mem::offset_of;
#[repr(C)]
struct Unoptimized {
flag: u8,
count: u64,
code: u16,
}
#[repr(C)]
struct Optimized {
count: u64,
code: u16,
flag: u8,
}
fn main() {
println!(
"Unoptimized: size = {}, offsets = ({}, {}, {})",
size_of::<Unoptimized>(),
offset_of!(Unoptimized, flag),
offset_of!(Unoptimized, count),
offset_of!(Unoptimized, code),
);
println!(
"Optimized: size = {}, offsets = ({}, {}, {})",
size_of::<Optimized>(),
offset_of!(Optimized, count),
offset_of!(Optimized, code),
offset_of!(Optimized, flag),
);
}
Unoptimized: size = 24, offsets = (0, 8, 16)
Optimized: size = 16, offsets = (0, 8, 10)
Eliminating Padding with #[repr(packed)]
The #[repr(packed)] (or #[repr(packed(N))]) attribute lowers the maximum field alignment to 1 (or N), removing internal and trailing padding.
Because fields inside a #[repr(packed)] struct may sit at unaligned memory addresses, taking a direct reference (&s.unaligned_field) is rejected by the compiler with error E0793 to prevent undefined behavior from unaligned pointer dereferences. Reading a field by value (let x = s.unaligned_field;) or taking a raw pointer (&raw const s.unaligned_field) is allowed.
// ❌ does not compile
#[repr(C, packed)]
struct PackedHeader {
tag: u8,
payload_len: u32,
}
fn main() {
let header = PackedHeader {
tag: 1,
payload_len: 1024,
};
let r: &u32 = &header.payload_len;
println!("{}", r);
}
error[E0793]: reference to packed field is unaligned
--> src/main.rs:14:19
|
14 | let r: &u32 = &header.payload_len;
| ^^^^^^^^^^^^^^^^^^^
|
= note: packed structs are only aligned by one byte, and many modern architectures penalize unaligned field accesses
= note: creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
= help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers)
Copying the field into a local variable or using std::ptr::addr_of! / &raw const with read_unaligned() accesses the packed field safely:
#[repr(C, packed)]
struct PackedHeader {
tag: u8,
payload_len: u32,
}
fn main() {
let header = PackedHeader {
tag: 1,
payload_len: 1024,
};
let len_copy: u32 = header.payload_len;
let raw_ptr: *const u32 = &raw const header.payload_len;
let len_via_ptr = unsafe { raw_ptr.read_unaligned() };
println!("PackedHeader size: {}", size_of::<PackedHeader>());
println!("len_copy = {}, len_via_ptr = {}", len_copy, len_via_ptr);
}
PackedHeader size: 5
len_copy = 1024, len_via_ptr = 1024
Padding Bytes and Raw Memory Transmutation
Padding bytes are uninitialized (undef) even when every field of a struct is initialized. Casting a struct with padding to a byte slice &[u8] or transmuting it to [u8; N] causes undefined behavior because u8 requires initialized bits. Only types with zero padding bytes and all-bit-valid fields can be safely viewed as raw byte slices.