0%

Solidity中的存储布局和内存布局

EVM 的整个空间被逻辑上划分为不同的区域,分别为栈(Stack)、内存(Memory)、存储(Storage)、代码(Code)和常量区域。

栈区域(Stack)区域

  • 特点:
    • 栈是一个 固定大小 的数据区域,最大为 1024 个字(每个字 32 字节)。
    • 用于存储操作数、临时值,以及函数调用的返回地址。
    • 数据只能通过 push 和 pop 操作访问。
  • 作用: 函数内部的临时变量和操作数首先存储在栈中。

内存 (Memory)区域

  • 功能: 用于临时存储数据,生命周期仅在交易或调用期间有效。
  • 地址范围: 逻辑地址从0开始,可以按需扩展,理论上可以扩展到 2^256 - 1
  • 特点:
    • 按 32 字节(256 位)对齐。
    • 初始化时为空(零值)。
    • 按需分配时会增加 gas 消耗。
  • 作用: 用于存储函数的局部变量、动态数组、字符串、函数参数等。

数据调用区域(Calldata)

  • 特点:
    • 是不可修改的只读区域,用于传递外部函数调用的输入数据。
    • 效率高,Gas 消耗低。
  • 作用:效率高,Gas 消耗低。

存储 (Storage)区域

  • 地址范围:存储区域的地址范围是从 0 到 2^256 - 1。
  • 存储单元:每个存储槽(storage slot)大小为 32 字节(256 位)。
  • 访问方式:存储按照 键值对的形式 存储,每个键为 256 位的存储槽地址,每个值为 256 位的存储内容。
  • 作用:存储合约的全局状态。

Storage Layout (存储布局)

存储状态布局涉及 合约的存储变量如何映射到 EVM 的存储空间。EVM 的存储空间是一个巨大的 256 位地址空间,采用键值对形式。每个存储槽(slot)存储 32 字节数据。

EVM 的存储区域采用 哈希映射结构 存储数据:

  • 简单变量:直接按照顺序存储,每个变量占用一个或部分存储槽。
  • 复杂数据结构:
    • 数组:第一个槽存储长度,数据存储在 keccak256(slot) 地址开始的连续存储槽中。
    • 映射:数据存储在 keccak256(key, slot) 地址处,key 是映射键,slot 是映射变量在合约中的存储槽编号。
    • 结构体:将结构体的成员顺序存储,每个成员分配单独的存储槽。

存储分配规则

  1. 基础规则

    • 变量顺序存储: 每个状态变量按声明顺序分配存储槽,优先填充当前槽剩余空间。
    • 一个槽的大小为 32 字节。
    • 如果变量类型大小超过槽大小(如 uint256),则占据一个完整槽。
    • 较小变量(如 uint8、bool)在同一槽中紧密打包,但不能跨槽。
  2. 映射(Mapping)

    • 映射的键值对(key-value)不会紧密打包在槽中。
    • 每个键(key)对应的值(value)存储在一个唯一的槽中,其地址由keccak256(key, slot) 计算
    • 键(key)不会存储在存储区域中,键值(key-value)对中的“键(key)”并没有单独存储的槽。所以无法直接遍历Mapping
  3. 动态数组与字符串

    • 元数据存储:动态数组的主槽(主存储槽)中存储的是数组的长度。
    • 数据存储:数组的实际数据存储在与主槽分开的连续存储槽中。
      • 数据槽的起始地址由 keccak256(slot) 计算得出,slot 是动态数组的主槽编号。
      • 数组中的每个元素按照顺序依次存储在数据槽中。
    • 字符串与动态数组处理相似。
  4. 结构体(Struct)

    • 结构体的变量按顺序存储。
    • 结构体变量紧密打包(类似单个槽中存储的变量)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.8.0;

contract StateLayoutExample {
uint256 public number; // Slot 0
bool public flag; // Slot 1 (packed with `smallValue` if space allows)
uint16 public smallValue; // Slot 1
mapping(uint256 => uint256) public map; // Data stored at keccak256(key, 2)
uint256[] public dynamicArray; // Slot 3: array length; data starts at keccak256(3)
struct MyStruct {
uint256 largeValue; // Slot 4
uint8 smallPart; // Slot 4 (packed with other struct fields)
}
MyStruct public myStruct; // Struct occupies its own slots
}

执行命令 solc --storage-layout --transient-storage-layout StateLayoutExample.sol 查看合约存储布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"storage":[
{"astId":3,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"number","offset":0,"slot":"0","type":"t_uint256"},
{"astId":5,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"flag","offset":0,"slot":"1","type":"t_bool"},
{"astId":7,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"smallValue","offset":1,"slot":"1","type":"t_uint16"},
{"astId":11,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"map","offset":0,"slot":"2","type":"t_mapping(t_uint256,t_uint256)"},
{"astId":14,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"dynamicArray","offset":0,"slot":"3","type":"t_array(t_uint256)dyn_storage"},
{"astId":22,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"myStruct","offset":0,"slot":"4","type":"t_struct(MyStruct)19_storage"}
],
"types":{
"t_array(t_uint256)dyn_storage":{"base":"t_uint256","encoding":"dynamic_array","label":"uint256[]","numberOfBytes":"32"},
"t_bool":{"encoding":"inplace","label":"bool","numberOfBytes":"1"},
"t_mapping(t_uint256,t_uint256)":{"encoding":"mapping","key":"t_uint256","label":"mapping(uint256 => uint256)","numberOfBytes":"32","value":"t_uint256"},
"t_struct(MyStruct)19_storage":{
"encoding":"inplace",
"label":"struct StateLayoutExample.MyStruct",
"members":[
{"astId":16,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"largeValue","offset":0,"slot":"0","type":"t_uint256"},
{"astId":18,"contract":"StateLayoutExample.sol:StateLayoutExample","label":"smallPart","offset":0,"slot":"1","type":"t_uint8"}
],
"numberOfBytes":"64"
},
"t_uint16":{"encoding":"inplace","label":"uint16","numberOfBytes":"2"},
"t_uint256":{"encoding":"inplace","label":"uint256","numberOfBytes":"32"},
"t_uint8":{"encoding":"inplace","label":"uint8","numberOfBytes":"1"}
}
}

storage

描述每个状态变量的存储位置:
- slot: 存储槽号。
- offset: 偏移量,表示变量在槽内的起始位置。
- type: 变量的类型标识,与 types 部分中的描述对应。

types

定义每种数据类型的详细信息:
- encoding: 数据的存储方式(如 inplace 或 dynamic_array)。
- numberOfBytes: 占用的字节数。
- members: 如果是结构体,列出所有成员及其存储位置。
- base: 如果是数组,表示基础类型。

Memory Layout (内存布局)

在 Solidity 中,内存(Memory) 是 EVM 提供的一种线性存储结构,主要用于在函数调用过程中存储临时变量、局部变量和中间计算结果。内存是按需分配的,不同于存储(Storage),内存是 瞬时的,在调用结束后会被释放,且读写成本较低。

内存是一个 按字节寻址的线性空间,地址范围从 0x00 到无限大(理论上为 2^256 - 1)。但在实际操作中,内存的分配是动态增长的,EVM 会根据需要分配内存,并且以 32 字节(一个字)为单位扩展。

- 每个内存地址存储一个字节。
- 每 32 个字节组成一个字(word),这是 EVM 操作的基本单位。

内存布局的设计遵循以下原则:

  1. 按需动态分配:
    • Solidity 会在需要时从内存中分配新的空间。
    • 起始空闲内存地址存储在 0x40,可通过 mload(0x40) 获取。
  2. 按 32 字节对齐:
    • 内存分配是以 32 字节为单位对齐的,即使是 bool 等小数据类型也会占用 32 字节。
  3. 变量存储位置:
    • 固定大小类型直接存储其值。
    • 动态类型存储一个指针,指向实际数据的内存地址。

固定分配区域

Solidity保留了四个32字节的插槽,具体的字节范围(包括端点)使用如下:

  • 0x00 ~ 0x3f(64Byte)
    • 用于哈希方法的临时空间
    • 临时空间可以在语句之间使用(即在内联汇编之中)。
  • 0x40 ~ 0x5f (Free memory pointer 32Byte):
    • mstore(0x40, …) 用于指向当前空闲内存的起始地址。
    • Solidity 使用内存时,会从该地址开始分配。
    • 开发者不需要手动管理,但在使用汇编时,需要注意维护 0x40 的正确值。
  • 0x60 ~ 0x7f(Zero pointer 32Byte):
    • 保留一块全零的内存区域,通常用于返回零值。
    • 0值插槽则用来对动态内存数组进行初始化,且永远不会写入数据 (因而可用的初始内存指针为 0x80)。

Solidity 总会把新对象保存在空闲内存指针的位置, 所以这段内存实际上从来不会空闲(在未来可能会修改这个机制)。

动态分配区域

Solidity 的局部变量、动态数组和字符串都从 0x80 开始动态分配,并按照实际需要扩展。

分配规则:
- 内存分配以 32 字节为单位对齐。
- 临时变量直接写入内存对应的位置。
- 动态类型需要额外存储元数据(如长度和起始地址)。

内存中的数据类型布局

固定大小的类型

  • 包括 uint256、address、bool 等。
  • 直接按 32 字节对齐存储,例如:
    • uint256 占用 32 字节。
    • bool 也占用 32 字节(尽管实际值只需 1 字节)。

动态类型

动态类型(如 string、bytes 和动态数组)会分为两个部分:

  1. 指针部分:主变量存储的是指向实际数据的偏移量(相对于起始地址 0x80 的偏移量)。
  2. 数据部分:存储动态类型的元数据(如长度)和实际数据。
1
2
3
4
5
6
7
8
9
pragma solidity ^0.8.0;

contract MemoryExample {
function demo() public pure returns (bytes memory, string memory) {
bytes memory byteArray = new bytes(10); // 动态字节数组
string memory str = "Hello, Memory!";
return (byteArray, str);
}
}
  1. byteArray:
    • 主变量存储数据偏移量,例如 0x80。
    • 数据存储从 0x80 开始:
      • 长度(10 字节):存储在 0x80。
      • 实际内容(10 字节):从 0xA0 开始。
  2. str:
    • 主变量存储数据偏移量,例如 0xE0。
    • 数据存储从 0xE0 开始:
      • 长度(14 字节):存储在 0xE0。
      • 实际内容(”Hello, Memory!”):从 0x100 开始。

State Layout 和 Memory Layout 的区别

特性 状态布局 (State Layout) 内存布局 (Memory Layout)
存储位置 持久化存储在 EVM 存储 (Storage) 中 函数调用时临时存储在 EVM 内存 (Memory) 中
生命周期 持久化,合约销毁之前一直存在 临时,函数执行结束即释放
成本 写入存储需要高 Gas 成本 使用内存成本较低
用途 存储合约的持久化数据 处理临时计算或函数的中间结果

Gas 优化建议

  • 状态变量优化
    • 紧密打包变量,减少存储槽的使用。
    • 避免在循环中频繁读写存储。
  • 内存优化
    • 使用临时变量代替状态变量进行中间计算。
    • 适时释放不再使用的内存。

了解 Solidity 的状态与内存布局,能帮助开发者高效设计合约,同时优化 Gas 成本。