跳转到内容

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 代替小整数

除非需要打包,单独的变量使用 uint256uint8 更便宜(EVM 会将小整数 ZERO-EXTEND 到 256 位):

// 函数参数和局部变量:uint256 更快
function process(uint256 value) public { /* ... */ }
// 结构体/状态变量:可以用小类型打包
struct Config {
uint128 maxAmount;
uint64 deadline;
uint32 fee; // 共 224 位,一个槽装下
bool active; // 1 位
}

3. 使用 constantimmutable

// 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 分析工具

Terminal window
# Foundry
forge test --gas-report
forge snapshot # 生成 Gas 快照基准
forge snapshot --diff # 对比优化前后
# Hardhat
npm 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 变量缓存到局部变量

深入阅读