合约优化 - 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] 核心要点
- 存储是最贵的 — 尽量减少和优化存储操作
- 变量打包 — 利用 32 字节的存储槽
- 使用 constant/immutable — 避免存储开销
- 缓存 storage — 多次读取时缓存到 memory
- 测试验证 — 使用 Remix、Hardhat 或 Foundry 测试 gas 消耗