Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the size of a tuple or struct not the sum of the members?

assert_eq!(12, mem::size_of::<(i32, f64)>()); // failed
assert_eq!(16, mem::size_of::<(i32, f64)>()); // succeed
assert_eq!(16, mem::size_of::<(i32, f64, i32)>()); // succeed

Why is it not 12 (4 + 8)? Does Rust have special treatment for tuples?

like image 937
wangliqiu2021 Avatar asked Jan 13 '21 18:01

wangliqiu2021


People also ask

Why isn't sizeof for a struct equal to the sum of sizeof of each member?

The sizeof for a struct is not always equal to the sum of sizeof of each individual member. This is because of the padding added by the compiler to avoid alignment issues. Padding is only added when a structure member is followed by a member with a larger size or at the end of the structure.

Why some structure variables occupy more size than expected?

It is because of memory alignment. By default memory is not aligned on one bye order and this happens. Memory is allocated on 4-byte chunks on 32bit systems.

What is size of struct always?

A struct that is aligned 4 will always be a multiple of 4 bytes even if the size of its members would be something that's not a multiple of 4 bytes.

What is the difference between tuple and struct?

So, use tuples when you want to return two or more arbitrary pieces of values from a function, but prefer structs when you have some fixed data you want to send or receive multiple times.


2 Answers

Why is it not 12 (4 + 8)? Does Rust have special treatment for tuples?

No. A regular struct can (and does) have the same "problem".

The answer is padding: on a 64-bit system, an f64 should be aligned to 8 bytes (that is, its starting address should be a multiple of 8). A structure normally has the alignment of its most constraining (largest-aligned) member, so the tuple has an alignment of 8.

This means your tuple must start at an address that's a multiple of 8, so the i32 starts at a multiple of 8, ends on a multiple of 4 (as it's 4 bytes), and the compiler adds 4 bytes of padding so the f64 is properly aligned:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[ i32 ] padding [     f64     ]

"But wait", you shout, "if I reverse the fields of my tuple the size doesn't change!".

That's true: the schema above is not accurate because by default rustc will reorder your fields to compact structures, so it will really do this:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[     f64     ] [ i32 ] padding 

which is why your third attempt is 16 bytes:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[     f64     ] [ i32 ] [ i32 ]

rather than 24:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[  32 ] padding [     f64     ] [  32 ] padding 

"Hold your horses" you say, keen eyed that you are, "I can see the alignment for the f64, but then why is there padding at the end? There's no f64 there!"

Well that's so the computer has an easier time with sequences: a struct with a given alignment should also have a size that's a multiple of its alignment, this way when you have multiple of them:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[     f64     ] [ i32 ] padding [     f64     ] [ i32 ] padding 

they're properly aligned and the computations of how to lay the next one are simple (just offset by the size of the struct), it also avoids putting this information everywhere. Basically, an array / vec is never itself padded, instead the padding is in the struct it stores. This allows packing to be a struct property and not infect arrays as well.


Using the repr(C) attribute, you can tell Rust to lay your structures in exactly the order you gave (it's not an option for tuples FWIW).

That is safe, and while it is not usually useful there are some edge-cases where it's important, those I know of (there are probably others) are:

  • Interfacing with foreign (FFI) code, which expects a very specific layout, that is in fact the origin of the flag's name (it makes Rust behave like C).
  • Avoiding false sharing in high-performance code.

You can also tell rustc to not pad the structure using repr(packed).

That is much riskier, it will generally degrade performances (most CPUs are rather cross with unaligned data) and might crash the program or return the wrong data entirely on some architectures. That is highly dependent on the CPU architecture, and on the system (OS) running on it: per the kernel's Unaligned Memory Accesses document

  1. Some architectures are able to perform unaligned memory accesses transparently, but there is usually a significant performance cost.
  2. Some architectures raise processor exceptions when unaligned accesses happen. The exception handler is able to correct the unaligned access, at significant cost to performance.
  3. Some architectures raise processor exceptions when unaligned accesses happen, but the exceptions do not contain enough information for the unaligned access to be corrected.
  4. Some architectures are not capable of unaligned memory access, but will silently perform a different memory access to the one that was requested, resulting in a subtle code bug that is hard to detect!

So "Class 1" architectures will perform the correct accesses, possibly at a performance cost.

"Class 2" architectures will perform the correct accesses, at a high performance cost (the CPU needs to call into the OS, and the unaligned access is converted into an aligned access in software), assuming the OS handles that case (it doesn't always in which case this resolves to a class 3 architecture).

"Class 3" architectures will kill the program on unaligned accesses (since the system has no way to fix it up.

"Class 4" will perform nonsense operations on unaligned accesses and are by far the worst.

An other common pitfall or unaligned access is that they tend to be non-atomic (since they need to expand into a sequence of aligned memory operations and manipulations of those), so you can get "torn" reads or writes even for otherwise atomic accesses.

like image 135
Masklinn Avatar answered Oct 12 '22 11:10

Masklinn


While @Masklinn already provided a general answer that it's due to alignment, here's The Rust Reference:

Size and Alignment

All values have an alignment and size.

The alignment of a value specifies what addresses are valid to store the value at. A value of alignment n must only be stored at an address that is a multiple of n. For example, a value with an alignment of 2 must be stored at an even address, while a value with an alignment of 1 can be stored at any address. Alignment is measured in bytes, and must be at least 1, and always a power of 2. The alignment of a value can be checked with the align_of_val function.

The size of a value is the offset in bytes between successive elements in an array with that item type including alignment padding. The size of a value is always a multiple of its alignment. The size of a value can be checked with the size_of_val function.

[...]

– The Rust Reference - Type Layout - Size and Alignment

(emphasis mine)

like image 21
vallentin Avatar answered Oct 12 '22 12:10

vallentin