Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-411 · Case file with fixtures · Case 383 of 694 · Runtime evidence

size_of_val on a Slice Uses Its Dynamic Length

std::mem::size_of_val measures the referenced value, including runtime metadata for dynamically sized slices. For &[T], the slice value is len times size_of::<T>; adding another reference measures the fat pointer instead.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The function measures the dynamically sized referent described by the reference, so slice length metadata determines how many element sizes contribute.
First discriminating check
Distinguish size_of_val(slice) from size_of_val(&slice), then assert the dynamic length-times-element-size relationship.

I passed a slice to size_of_val and expected the size of one element. Another common expectation is the size of the fat reference itself. The function reports neither: it measures the referenced slice value.

The failing fixture uses three u32 values. size_of_val reports twelve bytes.

The reference selects what gets measured

The function accepts &T where T may be dynamically sized. It returns the size of T, not the size of the argument expression used to reach it.

For this call:

let slice: &[u32] = &[1, 2, 3];
let bytes = std::mem::size_of_val(slice);

The inferred T is [u32]. Its runtime length is three, so the value size is:

3 elements * 4 bytes per u32 = 12 bytes

The repaired fixture asserts this length-times-element-size relationship rather than hard-coding only twelve.

One more reference changes the measured type

Calling size_of_val(&slice) is different. Now the outer reference points to a value whose type is &[u32]. That inner value is the fat slice reference itself.

assert_eq!(
    std::mem::size_of_val(&slice),
    std::mem::size_of::<&[u32]>(),
);

The placement of & changes the question from “how large is the slice data?” to “how large is the reference value?” I write the type beside measurements during debugging because visual syntax alone is easy to misread.

size_of is for statically sized types

std::mem::size_of::<T>() returns a compile-time size for Sized types. I can ask for size_of::<u32>() or size_of::<&[u32]>(), but not size_of::<[u32]>() because a bare slice has no compile-time length.

size_of_val uses the reference's metadata to answer for the particular dynamic value. Another slice of the same element type and length ten produces a different result.

This is one reason dynamically sized values must live behind a pointer-like type.

The result is not allocation size

For a borrowed slice, size_of_val reports the bytes occupied by its elements. It does not reveal allocator bookkeeping, the capacity of a source Vec, padding before or after an allocation, or bytes owned by nested pointers.

A slice of three String values reports the inline size of three String handles. It does not add the heap allocations containing their text.

Likewise, a slice taken from a Vec knows length but not Vec capacity. The spare allocation is not represented in slice metadata.

I call the result shallow value size unless the element type itself contains all data inline.

Zero-sized elements produce zero

A non-empty slice of a zero-sized type has dynamic length but occupies zero element bytes. The length remains important for iteration and destructor behavior even though size_of_val is zero.

This prevents a general inference from byte size to element count. bytes / size_of::<T>() is invalid when the element size is zero and can lose information even in other layouts.

I keep length as the authoritative collection count.

Trait objects use dynamic concrete layout

The Reference on dynamically sized types includes trait objects as another DST family. size_of_val on &dyn Trait uses pointer metadata to determine the size of the dynamic concrete value.

That still remains a shallow layout size. If the concrete value owns heap data, the referenced allocations are not recursively counted.

For memory profiling I use allocation instrumentation or a domain-specific deep-size routine with cycle and sharing rules. size_of_val cannot invent those ownership semantics.

Overflow and layout assumptions

Safe slices cannot have arbitrary impossible lengths; constructing one unsafely requires that total byte size and address range satisfy strict validity conditions. A forged length is not a harmless way to ask size_of_val a hypothetical question.

In safe code, the runtime calculation reflects an already valid referenced value. I do not create invalid raw slices to probe layout.

For serialization, I also avoid assuming native size_of_val equals encoded length. Endianness, padding, schema fields, and variable-length encodings make wire size a different contract.

My measurement inventory

Before using a size result, I name whether I need:

  • one element's inline size;
  • the dynamic slice's initialized bytes;
  • the fat pointer's own size;
  • a Vec's capacity allocation;
  • recursively owned heap bytes;
  • encoded or compressed bytes.

The core principle is that size belongs to a precise type and ownership layer. size_of_val(slice) measures the dynamically sized [T] selected by the reference. It uses runtime length metadata, while size_of_val(&slice) measures the reference value one layer above.