The Memory is one of the four data locations a solidity smart contract has (the others are : storage, calldata and stack). In simple words, it is the “heap” associated to the smart contract whenever it is running. Values are not persisted, as soon as the transaction is done, the memory content gets removed.
When a smart contract is called, the EVM will “instantiate” a memory space for it. This memory is byte addressable (as opposed to storage which is word addressable) and it is 2**256 bytes long, in other words, memory is a super long array of bytes.
Memory is used by solidity for multiple things like:
- Returning values to external calls.
- Setting arguments for external calls.
- Getting values from external calls.
- Revert with an error string.
- Log messages.
- Create other smart contracts.
- Use the keccak256 function
Solidity uses the first 4 memory words (32 bytes each -> first 128 bytes in total) in a very specific way:
- Words 0 and 1 (byte 0x00 to byte 0x3F) are used as “scratch space”, ephemeral space to write values (used for instance by the sha3 opcode).
- Word 3 (byte 0x40 to byte 0x5F) is used as “free memory pointer”, which contains the address of the last byte accessed in memory (by accessed, we mean written). Memory never gets cleaned (no garbage collector as in other programming languages), which means that the free memory pointer only increases and never decreases. When a contract gets executed, the free memory pointer first value is always 0x80.
- Word 4 (byte 0x60 to byte 0x7F) is always left empty.
It is important to note that if a developer adds YUL (in the form of an assembly block for instance) to its smart contract functions, he/she can use the memory array in which ever way he/she pleases, which means that there can be a risk of confusion / misinterpretation of the memory content if the contract also runs some solidity generated code….
Variables can be stored in memory in different ways, depending on their data type.
Value types are stored in memory just like in storage with the only difference that variables cannot be packed, each variable takes 32 bytes.
Structures are managed in the same way as value types, each variable within the struct takes 32 bytes, no packaging is performed.
Fixed size array work in a very similar way except that the first word (32 bytes) indicates the array length, that way the compiler can tell how many slots the array is using.
For the time being mappings and dynamically size arrays cannot be stored in memory.
On the other hand, bytes and strings can, and we can consider them as reference types too (since they are basically dynamically sized arrays of bytes).
The way solidity handles bytes/string in memory is just like it handles fixed size arrays, however when a bytes/string is “resized” (new byte/character added/removed) solidity completely recreates it in memory (without removing the previous copy of it, remember, nothing gets cleared in memory).
Memory gas fee depends on the amount of used memory, or in other words, the last accessed memory position (it does not matter if it was accessed to write or read a value) which is an incremental number since memory is never cleared…
Opcodes that access memory positions are: RETURN, REVERT, MLOAD, MSTORE, MSTORE8. Each of these opcodes have a fix gas fee (3 gas for MLOAD, …) plus a variable gas fee that can be added if they are accessing a memory position that was not accessed before.
The important thing to note here is that this “variable gas fee” increases linearly for the first 724 bytes of memory used, after that, the increase becomes exponential, which means that even if memory is supposed to be 2**256 bytes long, there are memory positions that cannot be reached, simply because the transaction would run out of gas…
- Respect the way in which solidity manages memory: If you only write solidity code, you do not have to care about the way in which solidity is using the memory, however, if you start embedding some YUL in your solidity functions, be careful not to mess with the memory (pay attention to the reserved spaces, always update the free memory pointer etc…).
- Do not use memory unless you really have to: Memory is cheaper than storage, however it is more expensive than calldata. If you are writing an external function that receives reference types arguments (struct, arrays), try to use the calldata location as much as you can, you should only use memory if you need to change the arguments values.