Memory in the Ethereum Virtual Machine (EVM) is a temporary, byte-addressable storage area that exists only during the execution of a smart contract. Understanding how memory operates is essential for writing efficient and secure Solidity contracts.
-
Byte Addressable:
- Memory is organized as a large array of bytes, which means each byte can be accessed individually.
-
Dynamic Size:
- Memory starts empty and expands dynamically as needed. The cost of memory expansion increases with the size of the memory accessed.
-
Temporary Nature:
- Memory is cleared after the function execution. It is not persistent, unlike storage.
-
Word Size:
- Memory operations generally involve 32-byte (256-bit) words.
Solidity reserves specific areas in memory for internal operations:
-
Scratch Space:
- The first 64 bytes (
0x00
to0x3F
) are used as scratch space for intermediate calculations (e.g., by thekeccak256
function).
- The first 64 bytes (
-
Free Memory Pointer:
- Located at
0x40
(64th byte). It stores the address of the first unused memory slot. Memory allocations should always reference and update this pointer.
- Located at
-
Empty Space:
- The region between
0x60
and the first free memory slot is reserved and not actively used.
- The region between
Memory is manipulated using specific EVM opcodes:
- Stores a 32-byte word at a specific memory location.
- Opcode:
MSTORE
.
PUSH1 0x10 // Value to store (16 in decimal)
PUSH1 0x00 // Memory address
MSTORE // Store 0x10 at memory address 0x00
- Loads a 32-byte word from a specific memory location.
- Opcode:
MLOAD
.
PUSH1 0x00 // Memory address
MLOAD // Load the value at address 0x00
- Stores a single byte at a specific memory location.
- Opcode:
MSTORE8
.
PUSH1 0xFF // Value to store (255 in decimal)
PUSH1 0x00 // Memory address
MSTORE8 // Store 0xFF at memory address 0x00
Memory expansion is not free and depends on the highest memory address accessed during a transaction. Key points:
-
Linear Growth (First 724 Bytes):
- Gas costs grow linearly for memory accessed up to 724 bytes.
-
Exponential Growth Beyond 724 Bytes:
- Accessing memory beyond 724 bytes causes gas costs to increase exponentially.
-
Implications:
- Avoid allocating excessively large memory arrays unless absolutely necessary.
- Value types (e.g.,
uint256
,bool
,address
) take up 32 bytes in memory.
uint256 x = 10; // Stored as a single 32-byte word in memory
-
Reference types (e.g.,
arrays
,structs
) have more complex memory layouts:- Static Arrays: Start with the length, followed by the elements.
- Dynamic Arrays and Strings: Start with the length, followed by a pointer to the actual data.
For a detailed implementation, refer to the DynamicArraysInStorage.sol file in the repository, which includes examples and assembly functions to explore dynamic arrays in Memory.
string memory example = "Hello"; // Length (5) + Characters (bytes of 'Hello')
-
Use Memory for Temporary Data:
- Memory is ideal for temporary computations and passing data between functions.
-
Prefer Calldata for Read-Only Data:
- When possible, use
calldata
for read-only function arguments, as it is cheaper than memory.
- When possible, use
-
Avoid Excessive Memory Expansion:
- Minimize operations that access high memory addresses to control gas costs.
-
Update the Free Memory Pointer:
- Always adjust the free memory pointer (
MSTORE 0x40
) when allocating new memory.
- Always adjust the free memory pointer (
This Solidity example demonstrates memory allocation and usage:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MemoryExample {
function allocateMemory() external pure returns (bytes memory) {
bytes memory temp = new bytes(10); // Allocate 10 bytes in memory
for (uint256 i = 0; i < temp.length; i++) {
temp[i] = bytes1(uint8(i + 65)); // Fill with ASCII characters (A-J)
}
return temp;
}
}
- Ethereum Yellow Paper: Formal definition of the EVM, including memory behavior.
- EVM Codes: Interactive opcode explorer for memory operations.
- Solidity Documentation: Comprehensive guide to memory usage in Solidity.
Understanding memory in the EVM is critical for writing efficient, gas-optimized contracts. By following best practices and leveraging memory effectively, developers can create secure and performant smart contracts.