You will find this topic probably as the most challenging. The table below is an overview of each data location available, with mention if read and write to it is allowed. For more details on each data location, read the section for each data location below.
Data Location | 🔍 Read | ✏️ Write |
---|---|---|
Storage | ✅ | ✅ |
Memory | ✅ | ✅ |
Calldata | ✅ | ❌ |
Stack | ✅ | ✅ |
Code | ✅ | ❌ |
When to use the keywords storage
, memory
and calldata
?
Any variable of complex type like array, struct, mapping or enum must specify a data location.
Storage refers to the contract's storage.
You can pass the keyword storage to a value you don't want to be cloned.
Storage is long-term but expensive !
In Ethereum, the memory holds temporary values (said not persisting), like function parameters.
Relative to the execution of the transaction of the transaction of the constructor.
Variables with the keyword memory assigned to them are therefore not persisting and temporary.
Moreover, memory variables are erased between external function calls.
Memory is short and cheap !
CallData is almost free but has a limited size !
The Stack hold small local variables. However, it can hold only a limited number of values. The Stack in Ethereum has maximum size of 1024 elements.
Finishes once a function finishes its execution.
The table below give the possible data locations for function parameters, depending on the function visibility.
Function visibility | storage |
memory |
calldata |
---|---|---|---|
external |
❌ not allowed | ✅ (since 0.6.9) | ✅ |
public |
❌ not allowed | ✅ | ✅ (since 0.6.9) |
internal |
✅ | ✅ | ✅ (since 0.6.9) |
private |
✅ | ✅ | ✅ (since 0.6.9) |
Function visibility | storage |
memory |
calldata |
---|---|---|---|
external |
✅ | ✅ | ✅ |
public |
✅ | ✅ | ✅ |
internal |
✅ | ✅ | ✅ |
private |
✅ | ✅ | ✅ |
Inside functions, all three data locations can be specified, no matter the function visibility. However, some specific rules exist for assignment between references, or to the data at the data location directly. The tables below summarize them.
For storage
:
storage references can be assigned a: |
|
---|---|
state variable directly | ✅ |
state variable via storage reference |
✅ |
memory reference |
❌ |
calldata value directly | ❌ |
calldata value via calldata reference |
❌ |
For memory
:
memory references can be assigned a: |
|
---|---|
state variable directly | ✅ |
state variable via storage reference |
✅ |
memory reference |
✅ |
calldata value directly | ✅ |
calldata value via calldata reference |
✅ |
For calldata
:
calldata references can be assigned a: |
|
---|---|
state variable directly | ❌ |
state variable via storage reference |
❌ |
memory reference |
❌ |
calldata value directly | ✅ |
calldata value via calldata reference |
✅ |
There are two main things to consider when specifying the data location inside the function body: the effect, and the gas usage.
Let's use a simple contract as an example to better understand. The contract holds a mapping of struct items in storage. To compare the behaviour of each data location, we will use different functions that use different data location keywords.
- a getter using
storage
. - a getter using
memory
. - a setter using
storage
. - a setter using
memory
.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;
contract Garage {
struct Item {
uint256 units;
}
mapping(uint256 => Item) items;
// gas (view) 24025
function getItemUnitsStorage(uint _itemIndex) public view returns(uint) {
Item storage item = items[_itemIndex];
return item.units;
}
// gas (view) 24055
function getItemUnitsMemory(uint _itemIndex) public view returns (uint) {
Item memory item = items[_itemIndex];
return item.units;
}
}
Let's debug the opcodes.
; getItemUnitsStorage = 30 instructions
PUSH1 00 ; 1) manipulate + prepare the stack
DUP1
PUSH1 00
DUP1
DUP5
DUP2
MSTORE ; 2.1) prepare the memory for hashing (1)
PUSH1 20
ADD
SWAP1
DUP2
MSTORE ; 2.2) prepare the memory for hashing (2)
PUSH1 20
ADD
PUSH1 00
SHA3 ; 3) compute the storage number to load via hashing
SWAP1
POP
DUP1
PUSH1 00
ADD
SLOAD ; 4) load mapping value from storage
SWAP2
POP
POP
SWAP2
SWAP1
POP
JUMP
JUMPDEST
; getItemUnitsMemory = 47 instructions
PUSH1 00
DUP1
PUSH1 00
DUP1
DUP5
DUP2
MSTORE
PUSH1 20
ADD
SWAP1
DUP2
MSTORE
PUSH1 20
ADD
PUSH1 00
SHA3
PUSH1 40 ; <------ additional opcodes start here
MLOAD ; 1) load the free memory pointer
DUP1 ; 2) reserve the free memory pointer by duplicating it
PUSH1 20
ADD ; 3) compute the new free memory pointer
PUSH1 40
MSTORE ; 4) store the new free memory pointer
SWAP1
DUP2
PUSH1 00
DUP3
ADD
SLOAD ; 5) load mapping value from storage
DUP2
MSTORE ; 6) store mapping value retrieved from storage in memory
POP
POP ; <------------ additonal opcodes end here
SWAP1
POP
DUP1
PUSH1 00
ADD
MLOAD
SWAP2
POP
POP
SWAP2
SWAP1
POP