It has to do with addressing storage. Let's break it down.
Consider a bool and an address. The bool takes 1 byte (limitation of optimization) and the address takes 20 bytes. So we need 21 bytes of storage. The EVM uses 32-byte words which are the smallest chunk of storage that is addressable.
If you go
contract A {
mapping(uint => bool) b;
mapping(uint => address) a;
}
a
and b
values live in separate "slots" and you can be accessed them individually. For this to work, all of the storage in a "slot" needs to be allocated for one value, so there is leftover space and it is twice as expensive to store as:
contract A {
struct Stuff {
bool b;
address a;
}
mapping(uint => Stuff) stuff;
Here, the compiler will look at the struct layout and work it out so one 32-byte word that holds 21-bytes of useful information. Of course, if you only want to update one member, then there is a read (to get the other) so the 32-byte write doesn't stomp on the other value.
Conceptually (not a technical explanation of the internals), something like this
Stuff storage s = Stuff[<uint>];
s.b = true;
makes the compiler
- compute the slot
- fetch the word
- replace the bool with true
- write the word
Tight packing is usually what you want because the total cost to write both a bool and an address is half of writing separate words. Words are the smallest unit of data for the expensive SSTORE opcode. So, yes, in a manner of speaking, structs help the compiler optimize the storage layout for gas cost.
Hope it helps.