So now that we've looked at some of the basics of assembly reads and writes memory, we can begin to look at how that memory is actually stored on each node. Everytime we make a variable in our solidity code, the evm stores it in a storage slot of 32 bytes (256 bits). That means that every time we have a uint (which is read as a uint256) we have packed a storage slot fully.
So lets look at some code:
uint128 a; uint256 b; uint128 c;
What the evm does is try to fit everything into storage slots sequentially, but since variable
b takes up an entire slot, it cannot fit in in the first one and needs to allocate a total of 3 32byte slots. If you instead order them so that the two smaller slots are next to each other, you can save one such storage operation. A more efficient code would look like this:
uint128 a; uint128 c; uint256 b;
That would allow for the EVM to only need to allocate two storage slots and 'pack' your variables.
Now let's look at the size of some common data types in Solidity:
uint256is 32 bytes
uint128is 16 bytes
uint64is 8 bytes
address payable) is 20 bytes
boolis 1 byte
stringis usually one byte per character
You can further break down a
uint into different sizes,
uint32... just keep in mind that your integer will overflow if you are using solidity version < 0.8.0 or your function will fail if you are using version > 0.8.0.
The largest number is calculated as
2^(number of bits) - 1, meaning
uint8 goes up to
((2^8) - 1) = 255 before your function fails.
I suggest keeping these in mind when you are designing structs.
A few small caveats, this only works when you are using
Structs (custom data objects) and/or declaring variables in your contract storage. Function arguments use memory which does not work in this way.
Also, if you will not be packing your structs this way, it actually can be more expensive to use a datatype which is smaller than the 32byte limit! So if you have something like
uint8 a; uint256 b;
It will be cheaper to make both variables
uint256! Counter intuitive, I know!
The reason for this is that the EVM reads 32 bytes at a time and will have to do some operations to make the data it is reading go down to 8 bits (1 byte) which is counter productive. Lastly, this only really applies when you are doing multiple read and writes to the same struct or storage slot in the same operation.
When you are writing code, you don't always want to optimize for efficiency, but for developer productivity and readability. Sometimes, it might be OK to do slightly less efficient code if it makes the project easier to maintain.
We will leave this here, stay tuned for part 4!