Gas 优化技巧
Gas 优化技巧
本页内容正在整理中,欢迎贡献。
概述
Gas 费用直接影响用户体验和合约竞争力。本文汇总 Solidity 合约 Gas 优化的核心技巧,从存储布局到汇编级别的优化,帮助你写出更经济高效的合约。
主要内容
Gas 成本速查表
| 操作 | Gas 消耗 |
|---|---|
| SSTORE(写入新槽) | 20,000 |
| SSTORE(修改现有槽) | 2,900 |
| SLOAD(读取存储槽) | 100(热)/ 2,100(冷) |
| MSTORE(写内存) | 3 |
| MLOAD(读内存) | 3 |
| CALL(外部调用) | 100(热)/ 2,600(冷) |
| ETH 转账 | 21,000(基础) |
存储优化(最重要)
1. 变量打包(Struct Packing)
EVM 存储以 32 字节为一个槽,将多个小变量放在同一槽中:
// ❌ 低效:占用 3 个存储槽contract Inefficient { uint256 a; // 槽 0(32 字节) uint128 b; // 槽 1(占半个槽,浪费) uint128 c; // 槽 2(另占半个槽,浪费)}
// ✅ 高效:只占用 2 个存储槽contract Efficient { uint256 a; // 槽 0 uint128 b; // 槽 1(前 16 字节) uint128 c; // 槽 1(后 16 字节,和 b 共享)}2. 使用 uint256 代替小整数
除非需要打包,单独的变量使用 uint256 比 uint8 更便宜(EVM 会将小整数 ZERO-EXTEND 到 256 位):
// 函数参数和局部变量:uint256 更快function process(uint256 value) public { /* ... */ }
// 结构体/状态变量:可以用小类型打包struct Config { uint128 maxAmount; uint64 deadline; uint32 fee; // 共 224 位,一个槽装下 bool active; // 1 位}3. 使用 constant 和 immutable
// constant:编译时常量,零 Gas 读取uint256 public constant MAX_SUPPLY = 10000;
// immutable:构造时设置,零 Gas 读取address public immutable owner;constructor() { owner = msg.sender; }
// 对比普通 storage 变量:每次读取需要 SLOAD(100+ Gas)4. 瞬态存储(EIP-1153)
// 使用 transient 关键字(Solidity 0.8.25+)// 比 SSTORE 便宜约 20x,交易结束自动清零bool transient private _locked;
modifier nonReentrant() { require(!_locked); _locked = true; _; _locked = false; // 免费(瞬态存储自动清除)}循环优化
// ❌ 低效function sumArray(uint256[] memory arr) public pure returns (uint256 total) { for (uint256 i = 0; i < arr.length; i++) { // arr.length 每次都读取 total += arr[i]; }}
// ✅ 高效function sumArray(uint256[] memory arr) public pure returns (uint256 total) { uint256 length = arr.length; // 缓存到局部变量 for (uint256 i = 0; i < length; ) { total += arr[i]; unchecked { ++i; } // unchecked 跳过溢出检查(已知安全),前缀 ++ 更便宜 }}自定义错误 vs require 字符串
// ❌ 高 Gas:字符串存储在字节码中require(msg.sender == owner, "Ownable: caller is not the owner");
// ✅ 低 Gas:自定义错误(约省 50 Gas)error NotOwner();error InvalidAmount(uint256 provided, uint256 maximum);
if (msg.sender != owner) revert NotOwner();if (amount > maxAmount) revert InvalidAmount(amount, maxAmount);calldata vs memory
// ❌ 外部函数参数使用 memory(会复制数据)function process(uint256[] memory data) external { /* ... */ }
// ✅ 外部函数参数使用 calldata(直接读取,不复制)function process(uint256[] calldata data) external { /* ... */ }映射 vs 数组
// mapping 读取:O(1),适合随机访问mapping(address => uint256) public balances; // 查询: 1 SLOAD
// 数组:适合有序迭代,但注意越界检查成本// 大数组迭代成本极高,考虑链下计算 + 链上验证Multicall 减少交易次数
// 将多个操作合并为一笔交易function multicall(bytes[] calldata data) external returns (bytes[] memory results) { results = new bytes[](data.length); for (uint256 i = 0; i < data.length; ) { (bool success, bytes memory result) = address(this).delegatecall(data[i]); require(success); results[i] = result; unchecked { ++i; } }}事件 vs 存储
// 历史数据使用 event 而非 storage(省 10-20 倍 Gas)// event 数据无法在合约内读取,但可通过 The Graph / RPC 查询
event Transfer(address indexed from, address indexed to, uint256 value);// 发出事件:约 375 Gas + 每字节 8 Gas// SSTORE:20,000 Gas(新写入)Gas 分析工具
# Foundryforge test --gas-reportforge snapshot # 生成 Gas 快照基准forge snapshot --diff # 对比优化前后
# Hardhatnpm install hardhat-gas-reporter# hardhat.config.ts:# gasReporter: { enabled: true, currency: "USD" }综合优化清单
- 使用
constant/immutable代替可变状态变量(如果不需要更改) - 结构体变量打包到 32 字节槽
- 循环中缓存
array.length,使用unchecked { ++i } - 外部函数参数使用
calldata而非memory - 用自定义错误替代
require字符串 - 历史/日志数据用
event而非storage - 重入锁考虑使用
transient storage - 频繁访问的 storage 变量缓存到局部变量
深入阅读
- Gas Puzzles —— 练习 Gas 优化
- RareSkills Gas 优化手册
- EIP-1153 瞬态存储 —— Dencun 升级引入