合约优化 - Gas 费优化指南

[!abstract] 核心概念
Gas 费是执行以太坊智能合约所需的计算费用,优化 Gas 费可以大幅降低用户交互成本和合约部署成本。


📌 核心概念

Gas 费是执行以太坊智能合约所需的计算费用,优化 Gas 费可以大幅降低用户交互成本和合约部署成本。


一、基础优化策略

1.1 变量类型优化

1
2
3
4
5
6
7
8
9
10
// ❌ 不推荐
uint256 public number1 = 100;
uint256 public number2 = 200;

// ✅ 推荐:使用更小的类型
uint128 public number1 = 100;
uint128 public number2 = 200; // 打包存储,节省存储槽

// 或者使用 uint8 处理小数值
uint8 public status = 1; // 状态值用最小的类型

1.2 变量打包(Packing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ 不推荐 - 占用 3 个存储槽
uint128 public a; // 槽 1
uint128 public b; // 槽 2
uint8 public c; // 槽 3

// ✅ 推荐 - 占用 2 个存储槽
struct PackedVars {
uint128 a;
uint128 b;
uint8 c;
}

// 或直接排列(注意:uint128 a 和 b 可以放在一个槽里)
uint128 public a;
uint128 public b; // a 和 b 共用一个槽(128+128=256)
uint8 public c; // c 单独占用槽 2(如果槽1已满)

[!info] 存储打包规则
EVM 存储槽为 32 字节(256 位)。连续定义的变量如果总大小 ≤ 32 字节,会被打包到同一个存储槽中,从而节省 Gas。

1.3 存储位置选择

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 错误:不能将 memory 赋值给 storage
function badExample(uint256[] memory _ids) public {
uint256[] storage ids = _ids; // 编译错误
}

// ✅ 推荐:明确指定存储位置
function goodExample(uint256[] calldata _ids) public {
uint256[] memory ids = _ids; // 使用 memory

// 复杂数据结构示例
uint256[] storage localArray = myArray; // 引用 storage,修改会影响原数据
uint256[] memory localArrayCopy = myArray; // 复制到 memory,独立修改
}

[!warning] 注意
不能将 memory 直接赋值给 storage 变量,必须明确指定存储位置。


二、进阶优化技巧

2.1 使用常量(Constant)

1
2
3
4
5
6
7
// ❌ 不推荐
uint256 public fee = 100;

// ✅ 推荐:常量不占用存储槽,直接内嵌到字节码
uint256 public constant FEE = 100;
string public constant TOKEN_NAME = "MyToken";
address public constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;

2.2 使用不可变量(Immutable)

1
2
3
4
5
6
7
8
9
10
11
// ❌ 不推荐
address public owner;
constructor(address _owner) {
owner = _owner; // 占用存储槽
}

// ✅ 推荐:immutable 变量部署后不可变,节省 gas
address public immutable owner;
constructor(address _owner) {
owner = _owner; // 存储在代码中,不占用存储槽
}

2.3 事件索引优化

1
2
3
4
5
// ❌ 不推荐:全部不索引
event Transfer(address from, address to, uint256 amount);

// ✅ 推荐:关键字段索引,便于查询
event Transfer(address indexed from, address indexed to, uint256 amount);

[!info] 提示
事件最多支持 3 个索引参数indexed 关键字)。


三、存储优化策略

3.1 存储读写优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ❌ 不推荐:多次读取 storage
function badRead() public view returns(uint256) {
uint256 sum = 0;
for(uint i = 0; i < myArray.length; i++) {
sum += myArray[i]; // 每次循环都读取 storage
}
return sum;
}

// ✅ 推荐:缓存到 memory
function goodRead() public view returns(uint256) {
uint256[] memory localArray = myArray; // 一次性读取到 memory
uint256 sum = 0;
for(uint i = 0; i < localArray.length; i++) {
sum += localArray[i]; // 从 memory 读取
}
return sum;
}

3.2 批量操作与位图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 不推荐:多次独立写入
function badBatch(address[] calldata _users) external {
for(uint i = 0; i < _users.length; i++) {
balances[_users[i]] = 100; // 每次写入都消耗 gas
}
}

// ✅ 推荐:使用位图存储多个状态(一个 uint256 存储 32 个 bool)
contract BitmapExample {
uint256 public states; // 一个 uint256 存储多个 bool 状态

function setState(uint8 _index, bool _value) external {
require(_index < 32, "超出范围");
if(_value) {
states |= (uint256(1) << _index); // 设置某位为 1
} else {
states &= ~(uint256(1) << _index); // 设置某位为 0
}
}

function getState(uint8 _index) external view returns(bool) {
return (states >> _index) & 1 == 1;
}
}

[!tip] 位图优势
位图可以将 32 个布尔状态存储在单个 uint256 中,相比使用 mapping(uint8 => bool) 可以节省大量存储空间和 Gas。


四、函数优化技巧

4.1 函数修饰符使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ 不推荐:多次重复检查
function withdraw() external {
require(balances[msg.sender] > 0, "余额不足");
require(block.timestamp > unlockTime, "未到解锁时间");
require(!paused, "合约暂停");
// 转账逻辑
}

// ✅ 推荐:使用修饰符复用检查逻辑
modifier onlyWhenActive() {
require(!paused, "合约暂停");
require(block.timestamp > unlockTime, "未到解锁时间");
_;
}

function withdraw() external onlyWhenActive {
require(balances[msg.sender] > 0, "余额不足");
// 转账逻辑
}

4.2 短路运算

1
2
3
4
5
// ❌ 不推荐:消耗多的放在前面
require(expensiveCheck() && simpleCheck());

// ✅ 推荐:简单的检查放前面,利用短路特性
require(simpleCheck() && expensiveCheck());

五、实用优化清单

🔧 部署优化

  • 使用构造函数初始化 immutable 变量
  • 使用 constant 常量代替 storage 变量
  • 优化合约继承结构,减少继承层级
  • 移除未使用的函数和变量

🔧 执行优化

  • 将 storage 变量缓存到 memory
  • 使用 unchecked 块处理不会溢出的计算
  • 使用 require 尽早检查失败条件
  • 合并多次外部调用

🔧 数据结构优化

  • 使用 mapping 替代数组(如果不需要遍历)
  • 使用 bytes32 替代 string(如果可能)
  • 紧凑打包存储变量

六、Gas 消耗对比表

操作类型 Gas 消耗 优化建议
SLOAD (读 storage) ~800 缓存到 memory
SSTORE (写 storage) ~5000-20000 减少写入次数
普通运算 ~3-10 影响较小
外部调用 ~700 尽量减少
创建合约 ~32000+ 优化构造函数

七、总结要点

[!summary] 核心要点

  1. 存储是最贵的 — 尽量减少和优化存储操作
  2. 变量打包 — 利用 32 字节的存储槽
  3. 使用 constant/immutable — 避免存储开销
  4. 缓存 storage — 多次读取时缓存到 memory
  5. 测试验证 — 使用 Remix、Hardhat 或 Foundry 测试 gas 消耗