🗓️ Week 10
This week builds on the work done previously on memory expansion. The goal is to understand the computational cost of memory expansion on the node.
Memory under the hood
EVM memory in geth is modeled by memory.go file. It is a simple struct that stores a byte array and the associated gas cost.
type Memory struct {
store []byte
lastGasCost uint64
}Memory.store is a slice. The in-memory representation of this struct is shown below:
+---------------------------+
| store (pointer) | 8 bytes
+---------------------------+
| lastGasCost | 8 bytes
+---------------------------+The pointer to store is a reference to the following information about the slice:
+---------------------------+
| (pointer to byte array) | 8 bytes
+---------------------------+
| slice length | 8 bytes
+---------------------------+
| slice capacity | 8 bytes
+---------------------------+Here is how this is laid out in memory:

The example below is a simplified mock memory from that of geth geth:
package main
import (
"fmt"
"sync"
"unsafe"
)
type Memory struct {
store []byte
lastGasCost uint64
}
var memoryPool = sync.Pool{
New: func() any {
return &Memory{}
},
}
func NewMemory() *Memory {
return memoryPool.Get().(*Memory)
}
func main() {
m := NewMemory();
m.store = make([]byte, 32) // Mock expansion
copied:=copy(m.store[0:], []byte("Hello Memory!")) // mock writing to memory
fmt.Println("Struct pointer", unsafe.Pointer(&m), copied);
// pause for debugging.
var input string
fmt.Scanln(&input)
}Running the file shows the pointer for the struct:
go run memory.go
Struct pointer 0xc000058020 13Using gdb, inspecting the pointer reveals the reference to the store slice:
(gdb) x/1xg 0xc000058020
0xc000058020: 0x000000c000076020The slice is a pointer to the underlying array, a length, and a capacity:
(gdb) x/3xg 0x000000c000076020
0xc000076020: 0x000000c00001a1e0 0x0000000000000020
0xc000076030: 0x0000000000000020And finally here is the byte array in memory:
(gdb) x/1sb 0x000000c00001a1e0
0xc00001a1e0: "Hello Memory!"And so the memory.go file is EVM's proxy to node's RAM. The MSTORE opcode under the hood does the following:
- Checks if the allocated memory store is sufficient to write the value.
- If insufficient, it allocates a new memory region and copies the existing memory store content over.
- Writes the value to memory store at the desired offset.
Is expansion costly?
To check this we expand the memory using zero values and write multiples of 1KB offset until we reach 100MB.
The snippet below is a modified version of geth's MSTORE benchmark that accepts an offset as a flag.
var memStartFlag uint64
func init() {
flag.Uint64Var(&memStartFlag, "memStart", 0x0, "Memory start value")
}
...
func BenchmarkOpMstore(bench *testing.B) {
var (
env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{})
stack = newstack()
mem = NewMemory()
evmInterpreter = NewEVMInterpreter(env)
)
env.interpreter = evmInterpreter
// Expand
mem.Resize(memStartFlag+32)
pc := uint64(0)
memStart := new(uint256.Int).SetUint64(memStartFlag)
v := "abcdef00000000000000abba000000000deaf000000c0de00100000000133700"
value := new(uint256.Int).SetBytes(common.Hex2Bytes(v))
bench.ResetTimer()
for i := 0; i < bench.N; i++ {
stack.push(value)
stack.push(memStart)
opMstore(&pc, evmInterpreter, &ScopeContext{mem, stack, nil})
}
}I used a script write to memory from 1KB to 100MB.
The graph of the result shows that, atleast for zero byte expansion, the write operation is not particularly costly.

Next steps
- Understand behavior for non-zero byte expansion.
- Get data on Mainnet memory usage.
📞 Calls
- Aug 12, 2024 - EPF Standup #9