Why does an enum require extra memory size? - rust

My understanding is that enum is like union in C and the system will allocate the largest of the data types in the enum.
enum E1 {
DblVal1(f64),
}
enum E2 {
DblVal1(f64),
DblVal2(f64),
DblVal3(f64),
DblVal4(f64),
}
fn main() {
println!("Size is {}", std::mem::size_of::<E1>());
println!("Size is {}", std::mem::size_of::<E2>());
}
Why does E1 takes up 8 bytes as expected, but E2 takes up 16 bytes?

In Rust, unlike in C, enums are tagged unions. That is, the enum knows which value it holds. So 8 bytes wouldn't be enough because there would be no room for the tag.

As a first approximation, you can assume that an enum is the size of the maximum of its variants plus a discriminant value to know which variant it is, rounded up to be efficiently aligned. The alignment depends on the platform.
This isn't always true; some types are "clever" and pack a bit tighter, such as Option<&T>. Your E1 is another example; it doesn't need a discriminant because there's only one possible value.
The actual memory layout of an enum is undefined and is up to the whim of the compiler. If you have an enum with variants that have no values, you can use a repr attribute to specify the total size.
You can also use a union in Rust. These do not have a tag/discriminant value and are the size of the largest variant (perhaps adding alignment as well). In exchange, these are unsafe to read as you can't be statically sure what variant it is.
See also:
How to specify the representation type for an enum in Rust to interface with C++?
Why does Nil increase one enum size but not another? How is memory allocated for Rust enums?
What is the overhead of Rust's Option type?
Can I use the "null pointer optimization" for my own non-pointer types?
Why does Rust not have unions?

Related

What references are fat? [duplicate]

I've read the term "fat pointer" in several contexts already, but I'm not sure what exactly it means and when it is used in Rust. The pointer seems to be twice as large as a normal pointer, but I don't understand why. It also seems to have something to do with trait objects.
The term "fat pointer" is used to refer to references and raw pointers to dynamically sized types (DSTs) – slices or trait objects. A fat pointer contains a pointer plus some information that makes the DST "complete" (e.g. the length).
Most commonly used types in Rust are not DSTs but have a fixed size known at compile time. These types implement the Sized trait. Even types that manage a heap buffer of dynamic size (like Vec<T>) are Sized, as the compiler knows the exact number of bytes a Vec<T> instance will take up on the stack. There are currently four different kinds of DSTs in Rust.
Slices ([T] and str)
The type [T] (for any T) is dynamically sized (so is the special "string slice" type str). That's why you usually only see it as &[T] or &mut [T], i.e. behind a reference. This reference is a so-called "fat pointer". Let's check:
dbg!(size_of::<&u32>());
dbg!(size_of::<&[u32; 2]>());
dbg!(size_of::<&[u32]>());
This prints (with some cleanup):
size_of::<&u32>() = 8
size_of::<&[u32; 2]>() = 8
size_of::<&[u32]>() = 16
So we see that a reference to a normal type like u32 is 8 bytes large, as is a reference to an array [u32; 2]. Those two types are not DSTs. But as [u32] is a DST, the reference to it is twice as large. In the case of slices, the additional data that "completes" the DST is simply the length. So one could say the representation of &[u32] is something like this:
struct SliceRef {
ptr: *const u32,
len: usize,
}
Trait objects (dyn Trait)
When using traits as trait objects (i.e. type erased, dynamically dispatched), these trait objects are DSTs. Example:
trait Animal {
fn speak(&self);
}
struct Cat;
impl Animal for Cat {
fn speak(&self) {
println!("meow");
}
}
dbg!(size_of::<&Cat>());
dbg!(size_of::<&dyn Animal>());
This prints (with some cleanup):
size_of::<&Cat>() = 8
size_of::<&dyn Animal>() = 16
Again, &Cat is only 8 bytes large because Cat is a normal type. But dyn Animal is a trait object and therefore dynamically sized. As such, &dyn Animal is 16 bytes large.
In the case of trait objects, the additional data that completes the DST is a pointer to the vtable (the vptr). I cannot fully explain the concept of vtables and vptrs here, but they are used to call the correct method implementation in this virtual dispatch context. The vtable is a static piece of data that basically only contains a function pointer for each method. With that, a reference to a trait object is basically represented as:
struct TraitObjectRef {
data_ptr: *const (),
vptr: *const (),
}
(This is different from C++, where the vptr for abstract classes is stored within the object. Both approaches have advantages and disadvantages.)
Custom DSTs
It's actually possible to create your own DSTs by having a struct where the last field is a DST. This is rather rare, though. One prominent example is std::path::Path.
A reference or pointer to the custom DST is also a fat pointer. The additional data depends on the kind of DST inside the struct.
Exception: Extern types
In RFC 1861, the extern type feature was introduced. Extern types are also DSTs, but pointers to them are not fat pointers. Or more exactly, as the RFC puts it:
In Rust, pointers to DSTs carry metadata about the object being pointed to. For strings and slices this is the length of the buffer, for trait objects this is the object's vtable. For extern types the metadata is simply (). This means that a pointer to an extern type has the same size as a usize (ie. it is not a "fat pointer").
But if you are not interacting with a C interface, you probably won't ever have to deal with these extern types.
Above, we've seen the sizes for immutable references. Fat pointers work the same for mutable references, immutable raw pointers and mutable raw pointers:
size_of::<&[u32]>() = 16
size_of::<&mut [u32]>() = 16
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>() = 16

What does it mean for all values of a type must use the same amount of memory in Rust?

Forgive me if there is an obvious answer to the question I'm asking, but I just don't quite understand it.
The Dynamically Sized Types and the Sized Trait section in chapter 19.3 Advanced Types of the 《The Rust Programming Language》 mentions:
Rust needs to know how much memory to allocate for any value of a particular type, and all values of a type must use the same amount of memory. If Rust allowed us to write this code, these two str values would need to take up the same amount of space. But they have different lengths: s1 needs 12 bytes of storage and s2 needs 15. This is why it’s not possible to create a variable holding a dynamically sized type.
When it says "and all values of a type must use the same amount of memory", it is meant to refer to dynamically sized types, not types such as vectors or arrays, right? v1 and v2 are also unlikely to occupy the same amount of memory.
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3, 4, 5, 6];
It's correct and considers vectors as well. A Vec<T> is roughly just a pointer to a position on the heap, a capacity, and a length. It could be defined, more or less, as
pub struct Vec<T>(T*, usize, usize);
And every value of that structure clearly has the same size. When Rust says that every value of a type has to have the same size, it only refers to the size of the structure itself, not to the recursive size of all things it points to. Box<T> has a constant size, regardless of T, which is why Box can hold even things that are dynamically sized, such as trait objects. Likewise, String is basically just a pointer.
Likewise, if we define
pub enum MyEnum {
A(i32),
B(i32, i32),
}
Then MyEnum::A is no smaller than MyEnum::B, for similar reasons, despite the latter having more data than the former.
Every type that can be stored and accessed without the indirection of a reference or Box must have the Sized trait implemented. This means that every instance of the type will have the same size. A str is a DST, as the data it holds can be of a variable length, and thus, you can only access strs as references, or from String, which holds the str data on the heap, through a pointer.
Every Vec also takes the same space, which is 24 bytes on a 64-bit machine.
For example:
let vec = vec![1, 2, 3, 4];
println!("{}", std::mem::size_of_val(&vec)); // Prints '24'.

Size of raw pointer of array in Rust [duplicate]

I've read the term "fat pointer" in several contexts already, but I'm not sure what exactly it means and when it is used in Rust. The pointer seems to be twice as large as a normal pointer, but I don't understand why. It also seems to have something to do with trait objects.
The term "fat pointer" is used to refer to references and raw pointers to dynamically sized types (DSTs) – slices or trait objects. A fat pointer contains a pointer plus some information that makes the DST "complete" (e.g. the length).
Most commonly used types in Rust are not DSTs but have a fixed size known at compile time. These types implement the Sized trait. Even types that manage a heap buffer of dynamic size (like Vec<T>) are Sized, as the compiler knows the exact number of bytes a Vec<T> instance will take up on the stack. There are currently four different kinds of DSTs in Rust.
Slices ([T] and str)
The type [T] (for any T) is dynamically sized (so is the special "string slice" type str). That's why you usually only see it as &[T] or &mut [T], i.e. behind a reference. This reference is a so-called "fat pointer". Let's check:
dbg!(size_of::<&u32>());
dbg!(size_of::<&[u32; 2]>());
dbg!(size_of::<&[u32]>());
This prints (with some cleanup):
size_of::<&u32>() = 8
size_of::<&[u32; 2]>() = 8
size_of::<&[u32]>() = 16
So we see that a reference to a normal type like u32 is 8 bytes large, as is a reference to an array [u32; 2]. Those two types are not DSTs. But as [u32] is a DST, the reference to it is twice as large. In the case of slices, the additional data that "completes" the DST is simply the length. So one could say the representation of &[u32] is something like this:
struct SliceRef {
ptr: *const u32,
len: usize,
}
Trait objects (dyn Trait)
When using traits as trait objects (i.e. type erased, dynamically dispatched), these trait objects are DSTs. Example:
trait Animal {
fn speak(&self);
}
struct Cat;
impl Animal for Cat {
fn speak(&self) {
println!("meow");
}
}
dbg!(size_of::<&Cat>());
dbg!(size_of::<&dyn Animal>());
This prints (with some cleanup):
size_of::<&Cat>() = 8
size_of::<&dyn Animal>() = 16
Again, &Cat is only 8 bytes large because Cat is a normal type. But dyn Animal is a trait object and therefore dynamically sized. As such, &dyn Animal is 16 bytes large.
In the case of trait objects, the additional data that completes the DST is a pointer to the vtable (the vptr). I cannot fully explain the concept of vtables and vptrs here, but they are used to call the correct method implementation in this virtual dispatch context. The vtable is a static piece of data that basically only contains a function pointer for each method. With that, a reference to a trait object is basically represented as:
struct TraitObjectRef {
data_ptr: *const (),
vptr: *const (),
}
(This is different from C++, where the vptr for abstract classes is stored within the object. Both approaches have advantages and disadvantages.)
Custom DSTs
It's actually possible to create your own DSTs by having a struct where the last field is a DST. This is rather rare, though. One prominent example is std::path::Path.
A reference or pointer to the custom DST is also a fat pointer. The additional data depends on the kind of DST inside the struct.
Exception: Extern types
In RFC 1861, the extern type feature was introduced. Extern types are also DSTs, but pointers to them are not fat pointers. Or more exactly, as the RFC puts it:
In Rust, pointers to DSTs carry metadata about the object being pointed to. For strings and slices this is the length of the buffer, for trait objects this is the object's vtable. For extern types the metadata is simply (). This means that a pointer to an extern type has the same size as a usize (ie. it is not a "fat pointer").
But if you are not interacting with a C interface, you probably won't ever have to deal with these extern types.
Above, we've seen the sizes for immutable references. Fat pointers work the same for mutable references, immutable raw pointers and mutable raw pointers:
size_of::<&[u32]>() = 16
size_of::<&mut [u32]>() = 16
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>() = 16

What is the null pointer optimization in Rust?

In Learning Rust With Entirely Too Many Linked Lists, the author mentions:
However, if we have a special kind of enum:
enum Foo {
A,
B(ContainsANonNullPtr),
}
the null pointer optimization kicks in, which eliminates the space needed for the tag. If the variant is A, the whole enum is set to all 0's. Otherwise, the variant is B. This works because B can never be all 0's, since it contains a non-zero pointer.
I guess that the author is saying that (assuming A is 4 bits, and B is 4 bits)
let test = Foo::A
the memory layout is
0000 0000
but
let test = Foo::B
the memory layout is
some 8 bit non 0 value
What exactly is optimized here? Aren't both representation always 8 bits What does it mean when the author claims
It means &, &mut, Box, Rc, Arc, Vec, and several other important types in Rust have no overhead when put in an Option
The null pointer optimization basically means that if you have an enum with two variants, where one variant has no associated data, and the other variant has associated data where the bit pattern of all zeros isn't a valid value, then the enum itself will take exactly the same amount of space as that associated value, using the all zeroes bit pattern to indicate that it's the other variant.
In other words, this means that Option<&T> is exactly the same size as &T instead of requiring an extra word.
enum is a tagged union. Without optimization it looks like
Foo::A; // tag 0x00 data 0xXX
Foo::B(2); // tag 0x01 data 0x02
The null pointer optimization removes the separate tag field.
Foo::A; // tag+data 0x00
Foo::B(2); // tag+data 0x02
I m also learning too many linked list, perhaps this code snippet can deepen your understanding
pub enum WithNullPtrOptimization{
A,
B(String),
}
pub enum WithoutNullPtrOptimization{
A,
B(u32),
}
fn main() {
println!("{} {}", std::mem::size_of::<WithNullPtrOptimization>(), std::mem::size_of::<String>()); // 24 24
println!("{} {}", std::mem::size_of::<WithoutNullPtrOptimization>(), std::mem::size_of::<u32>()); // 8 4
}

Is Option<T> optimized to a single byte when T allows it?

Suppose we have an enum Foo { A, B, C }.
Is an Option<Foo> optimized to a single byte in this case?
Bonus question: if so, what are the limits of the optimization process? Enums can be nested and contain other types. Is the compiler always capable of calculating the maximum number of combinations and then choosing the smallest representation?
The compiler is not very smart when it comes to optimizing the layout of enums for space. Given:
enum Option<T> { None, Some(T) }
enum Weird<T> { Nil, NotNil { x: int, y: T } }
enum Foo { A, B, C }
There's really only one case the compiler considers:
An Option-like enum: one variant carrying no data ("nullary"), one variant containing exactly one datum. When used with a pointer known to never be null (currently, only references and Box<T>) the representation will be that of a single pointer, null indicating the nullary variant. As a special case, Weird will receive the same treatment, but the value of the y field will be used to determine which variant the value represents.
Beyond this, there are many, many possible optimizations available, but the compiler doesn't do them yet. In particular, your case will not berepresented as a single byte. For a single enum, not considering the nested case, it will be represented as the smallest integer it can.

Resources