Solidity — Storage vs Memory vs Calldata

YBM
Coinmonks
Published in
4 min readApr 20, 2022

--

Photo by Steve Johnson on Unsplash

Whenever you are writing smart contracts in Solidity, you must be cognizant of how your variables and data are handled by the EVM. The choices you make will influence, among other things, gas costs — to call your functions or deploy your contract — as well as storage layout.

Given that every bit of block space in Ethereum is highly valued (hence the cost of Eth, albeit falling recently), this heavily influences the efficiency of your code and resultant contract, which hopefully will be invoked frequently. Every function call uses gas; every bit of savings helps when summing over the potential lifetime of total function invocations.

Storage

Storage is the easiest to grasp — it is where all state variables are stored. Because state can be altered in a contract (for example, within a function), storage variables must be mutable. However, their location is persistent, and they are stored on the blockchain.

State variables in storage are arranged in a compact way — if possible, multiple values will occupy the same storage slot. Besides the special cases of dynamically sized arrays and structs, other variables are packed together in blocks of 32 bytes.

If these variables are less than 32 bytes, they will be combined to occupy the same slot. Otherwise, they will be pushed onto the next storage slot. The data is stored contiguously (ie, one after the other), starting from the 0 slot (to slots 1, 2, 3, etc), in order of their declaration in the contract.

Dynamic arrays and structs always occupy a new slot, and any variables following them will also be initialized to start a new storage slot.

Because the size of both dynamic arrays and structs are unknown a priori (ie until you assign them later in your contract) they cannot be stored with their data in between other state variables. Instead, they are assumed to take up 32 bytes, and the elements within them are stored starting at a separate storage slot that is computed using a Keccak-256 hash.

However, constant state variables are not saved into a storage slot. Rather, they are injected directly into the contract bytecode — whenever those variables are read, the contract automatically switches them out for their assigned constant value.

Memory

Memory is reserved for variables that are defined within the scope of a function. They only persist while a function is called, and thus are temporary variables that cannot be accessed outside this scope (ie anywhere else in your contract besides within that function). However, they are mutable within that function.

Solidity reserves four 32-byte slots for memory, with specific byte ranges, consisting of: 1) 64-byte scratch space for hashing methods; 2) 32 bytes for currently allocated memory size, which is the free memory pointer where Solidity always places new objects; and 3) a 32-byte zero slot — which is used as the initial value for dynamic memory arrays and should never be written to.

Because of these layout differences, there are situations for arrays and structs where they will occupy different amounts of space depending on being either in storage or memory.

Example:

uint8[4] arr;struct Str {
uint v1;
uint v2;
uint8 v3;
uint8 v4;
}

In both cases, the array arr and the struct Str occupy 128 bytes in memory (ie 4 items, 32 bytes each). However, as storage, arr only occupies 32 bytes (1 slot) while Str occupies 96 bytes (3 slots, 32 bytes each).

Calldata

Calldata is an immutable, temporary location where function arguments are stored, and behaves mostly like memory.

It is recommended to try to use calldata because it avoids unnecessary copies and ensures that the data is unaltered. Arrays and structs with calldata data location can also be returned from functions.

This type of data is assumed to be in a format defined by the ABI specification, ie padded to multiples of 32 bytes (which differs from internal function calls). Arguments for constructors are slightly different, as they are directly appended to the end of the contract’s code (also in ABI encoding).

Comparisons

Whenever you define a reference type variable (array or struct) you will also need to define its data location — unless it’s a state variable, in which case it is automatically interpreted as storage. Since Solidity v0.6.9, memory and calldata are allowed in all functions regardless of their visibility type (ie external, public, etc).

Assignments will either result in copies being created, or mere references to the same piece of data — similar to objects or arrays in Javascript:

  • Assignments between storage and memory (or from calldata) always create a separate copy.
  • Assignments from memory to memory only create references. Therefore changing one memory variable alters all other memory variables that refer to the same data.
  • Assignments from storage to a local storage variable also only assign a reference.
  • All other assignments to storage always copy.

For array parameters in functions, it is recommended to use calldata over memory , as this provides significant gas savings. For example, a summing function that loops over an input array can save roughly 1829 gas (~3.5%) by using calldata.

// Gas used: 50992
function func1 (uint[] memory nums) external {
for (uint i = 0; i < nums.length; ++i) {
...
}
}
// Gas used: 49163
function func2 (uint[] calldata nums) external {
for (uint i = 0; i < nums.length; ++i) {
...
}
}

And that’s it — some info regarding data locations in Solidity. Thanks for reading!

--

--

YBM
Coinmonks

blockchain, crypto, web3, software, Lakers, anything that interests me. Any views expressed are the personal views of the author.