在以太坊里,Gas 是每个人必须理解的核心概念。本文主要讨论如何估算和优化 Gas,帮助开发者们能够写出更节能的区块链应用。
Gas 是什么
Gas 是以太坊里用来衡量计算资源消耗的单位。在以太坊上执行写操作(例如转账)都得需要消耗一定数量的 Gas,读操作(例如查询余额)一般不需要消耗 Gas。每次写操作消耗的 Gas 费用为:Gas 数量 * Gas 价格 = 交易费用。这种机制设计有两个核心目的:
- 防止网络滥用:通过为每个写操作设置成本,防止恶意行为者通过执行无限循环或资源密集型操作来攻击网络。
- 激励验证者:为网络维护者提供经济激励,补偿他们验证交易和执行计算所花费的资源。
简而言之,Gas 是以太坊的"燃料",使整个网络能够安全、有序地运行。EVM 会追踪每个交易的总 Gas 消耗,确保不超过用户设置的 Gas 限制。如果交易执行过程中 Gas 用尽,交易将回滚(所有更改都会撤销),但已使用的 Gas 仍会被收取。
如何估算 Gas
通常来说 Gas 数量是能够预估的(模糊预估,一个大概值),而 Gas 价格是不能预估的。为什么呢?因为以太坊虚拟机(EVM)对每一条指令(如 ADD、SSTORE、CALL)都预先定义了固定的 Gas 消耗值。而 Gas 价格不能预估主要是因为价格由市场供需、网络拥堵、矿工选择和 EIP-1559 动态费用机制共同决定的,无法精准预测,这里不详细说了。
在预估 Gas 数量之前,我们先来看一下一些常见的 Gas 操作消耗的 Gas 数量:
存储操作:
- SLOAD(读取存储): ~2100 Gas (冷访问)/ 100 Gas(热访问)-- 第一次访问是冷访问,后续都是热访问
- SSTORE(首次写入): ~20000 Gas
- SSTORE(修改现有值): ~5000 Gas
- SSTORE(清零): 可获得退款(但受EIP-3529限制)
计算操作:
- ADD/SUB: 3 Gas
- MUL/DIV: 5 Gas
- 比较运算: 3 Gas
- OR: 3 Gas
调用操作:
- CALL(普通调用): 基础700 Gas + 变动成本
- DELEGATECALL: 基础700 Gas + 变动成本
- CREATE(合约创建): 32,000 Gas + 代码成本
不同类型交易的基础费用:
- 普通 ETH 转账:21000 Gas (这是以太坊协议规定的基础交易成本,用于支付交易签名验证和状态变更)
- 合约调用:21000 Gas + 函数执行费用
- 合约创建:21000 Gas + 32000 Gas + 代码存储费用
了解了一些常见操作消耗的 Gas 数量后,我们再来看看下面的示例。
示例:简单的代币转账函数
假设我们有一个 ERC-20 代币转账函数:
ts
function transfer(address recipient, uint256 amount) external override returns (bool) {
if (_balances[msg.sender] < amount) {
revert InsufficientBalance(_balances[msg.sender], amount);
}
_balances[msg.sender] -= amount;
_balances[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
这个函数大概的 Gas 消耗如下所示:
-
普通 ETH 转账交易基础费用:21000 Gas
-
余额检查:
- SLOAD 读取
_balances[msg.sender]
(冷访问): 2100 Gas - 比较操作 (<): 3 Gas
- SLOAD 读取
-
余额更新:
- SLOAD 读取
_balances[msg.sender]
(热访问): 100 Gas - SSTORE 更新
_balances[msg.sender]
: 5000 Gas - SLOAD 读取
_balances[recipient]
(冷访问): 2100 Gas - SSTORE 更新
_balances[recipient]
(假设首次写入): 20000 Gas
- SLOAD 读取
-
事件发送:
- LOG3 (Transfer 事件): ~1500 Gas
-
其他开销:
- 函数调用和返回: ~200 Gas
- 参数编码/解码: ~200 Gas
Gas 总消耗:基础开销 + 函数操作 = 约 52203 Gas
当然,在实际执行时,根据具体状态(例如地址是否曾被访问过,存储位置是否已有值等)的不同,所消耗的 Gas 数量也会有所不同。
现在,让我们把这个合约部署到测试网执行一下 transfer
操作试试。如下图所示,可以发现转账总共使用了 52334 Gas,和预估的差不多。
为什么Gas无法精确预估
虽然我们可以通过了解EVM操作码的基本Gas成本来估算函数的Gas消耗,但实际上无法精确预估具体交易的Gas用量,主要原因包括:
1. 状态依赖性
同一函数在不同状态下消耗的Gas会有所不同。例如,写入一个已有值的存储槽比写入空槽消耗少 15000 Gas,我们无法预先知道合约部署到主网后的精确状态。
2. 热/冷访问差异
基于EIP-2929,首次访问地址或存储槽(冷访问)比后续访问(热访问)贵得多。交易执行路径不同时,热/冷访问的模式也会变化,在复杂交易中,很难预测哪些访问是热的,哪些是冷的。
3. 执行上下文变化
Gas消耗受到区块链当前状态的影响,同一函数可能在不同区块链状态下有不同的执行路径,特别是依赖外部条件的函数(如时间戳、区块高度等)。
4. 退款机制的不确定性
存储清零操作可获得退款,但总退款受限于交易Gas消耗的1/5,复杂交易中退款上限可能会变化,难以准确计算。
5. 动态计算的不可预测性
- 循环次数、条件分支等在运行前无法确定
- 输入参数大小(如数组长度、字符串长度)直接影响Gas消耗
- 某些密码学操作(如keccak256)Gas消耗与输入数据内容相关
基于以上原因,我们很难准确的预估复杂的合约所消耗的 Gas 数量。
使用工具进行预估
即使 Gas 无法精确预估,但是我们还是可以借助工具来做一个大概的估算。例如我们可以使用 Hardhat 和 Foundry 来写测试代码进行 Gas 估算。
这次我们换一个复杂点的 transfer
函数来进行测试,合约代码如下:
ts
function _transfer(address sender, address recipient, uint256 amount) internal {
if (sender == address(0) || recipient == address(0)) revert TransferToZeroAddress();
if (_balances[sender] < amount) revert InsufficientBalance(_balances[sender], amount);
_balances[sender] -= amount;
_balances[recipient] += amount;
lastTransferTime[sender] = block.timestamp;
emit Transfer(sender, recipient, amount);
}
function transfer(
address recipient,
uint256 amount
)
external
override
whenNotPaused
notBlacklisted(msg.sender, recipient)
checkCooldown(msg.sender)
nonReentrant
returns (bool)
{
_transfer(msg.sender, recipient, amount);
return true;
}
这个 transfer
函数比之前的版本复杂了很多,上面使用了很多修饰符,而且还内部调用了 _transfer
函数,要是手动来算会比较费劲。所以这次打算使用 Hardhat 和 Foundry 来测试,这两个都是可以用来写 Solidity 测试的工具。
可以看到上面一共有三张图,第一张图片是真实转账的图片,消耗了 83020 Gas;第二张图是 Hardhat 测试消耗的 Gas 数量,最大值为 85244;第三张图是 Foundry 预估的 Gas 数量,为 66836。
总的来说,用工具来估算 Gas 是一种比较可行的方法,虽然不是完全准确,但是出入不会很大。
优化 Gas
1. 存储优化策略
存储操作是 EVM 中最昂贵的操作之一,优化它们可以极大的降低 Gas 成本。
使用 packing 打包变量:
ts
// 未优化: 3个槽位,每次写入 20000 Gas
uint256 a; // 槽位 0
uint8 b; // 槽位 1
bool c; // 槽位 2
// 优化后: 1个槽位,单次写入 20000 Gas
struct PackedData {
uint8 b; // 1字节
bool c; // 1字节
uint240 _unused; // 填充剩余空间
}
uint256 a; // 槽位 0
PackedData d; // 还是槽位 0
优化存储布局:
- 将频繁一起访问的变量放在同一槽位
- 利用Solidity的紧凑存储特性
- 对结构体字段排序,实现最紧凑布局
减少存储写入次数:
ts
// 未优化: 2次SSTORE (40,000+ Gas)
function updateValues(uint256 a, uint256 b) external {
value1 = a;
value2 = b;
}
// 优化: 使用内存变量累积更改,1次SSTORE (~20,000 Gas)
function updateValues(uint256 a, uint256 b) external {
Values memory values = Values(a, b);
combinedValues = values;
}
使用映射替代数组:
ts
// 未优化: 数组需要按顺序存储,增加元素可能需要复制整个数组
uint256[] public values;
// 优化: 映射不要求连续存储,节省重新排列成本
mapping(uint256 => uint256) public values;
uint256 public valueCount;
function addValue(uint256 value) external {
values[valueCount] = value;
valueCount++;
}
缓存存储变量到内存:
ts
// 未优化: 多次访问存储变量 (每次SLOAD消耗~2100 Gas)
function sumStorageArray() public view returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < myArray.length; i++) {
sum += myArray[i];
}
return sum;
}
// 优化: 将存储数组加载到内存中 (一次性SLOAD成本 + 低成本内存访问)
function sumStorageArray() public view returns (uint256) {
uint256[] memory array = myArray;
uint256 sum = 0;
for (uint256 i = 0; i < array.length; i++) {
sum += array[i];
}
return sum;
}
计算优化策略
计算优化可减少合约的 Gas 消耗。
使用位操作替代算术运算:
ts
// 未优化: 乘法/除法 (5 Gas)
uint256 n = x * 2;
uint256 m = y / 2;
// 优化: 位操作 (3 Gas)
uint256 n = x << 1;
uint256 m = y >> 1;
使用 unchecked 块: 自 Solidity 0.8.0 起,可使用 unchecked 跳过溢出检查,在确定不会溢出的场景中节省 Gas:
ts
// 带溢出检查 (~15 Gas/迭代)
for (uint256 i = 0; i < length; i++) {
// 代码
}
// 无溢出检查 (~5 Gas/迭代)
for (uint256 i = 0; i < length;) {
// 代码
unchecked { i++; }
}
短路求值优化:
ts
// 未优化: 即使第一个条件为false,仍会评估所有条件
function processIfValid(uint256 value) public {
bool condition1 = expensiveCheck1(value);
bool condition2 = expensiveCheck2(value);
bool condition3 = expensiveCheck3(value);
if (condition1 && condition2 && condition3) {
// 处理有效值
}
}
// 优化: 使用短路求值避免不必要的检查
function processIfValid(uint256 value) public {
if (expensiveCheck1(value) && expensiveCheck2(value) && expensiveCheck3(value)) {
// 处理有效值
}
}
避免不必要的计算:
ts
// 未优化: 在循环中重复计算不变量
function processList(uint256[] memory values) public {
for (uint256 i = 0; i < values.length; i++) {
values[i] = values[i] * values.length + someConstant;
}
}
// 优化: 提取循环不变量
function processList(uint256[] memory values) public {
uint256 length = values.length;
uint256 factor = length + someConstant;
for (uint256 i = 0; i < length; i++) {
values[i] = values[i] * factor;
}
}
数据类型和操作优化
使用较小的整数类型:
ts
// 未优化: 默认使用uint256,即使较小值足够
function processSmallNumbers(uint256 small1, uint256 small2) public {
require(small1 <= 100);
require(small2 <= 100);
// 处理小数字
}
// 优化: 使用恰当大小的整数类型
function processSmallNumbers(uint8 small1, uint8 small2) public {
// uint8最大值为255,足够容纳100
// 处理小数字
}
固定长度数组优于动态数组:
ts
// 未优化: 动态数组需要额外存储长度
uint256[] public dynamicArray;
// 优化: 固定长度数组不需要存储长度
uint256[10] public fixedArray;
使用 bytes 替代 string:
ts
// 未优化: 存储字符串
string public identifier = "Contract ID";
// 优化: 对于32字节以内的数据,bytes32比string更高效
bytes32 public identifier = "Contract ID";
优化枚举类型:
ts
// 未优化: 默认枚举从0开始
enum Status {
Active,
Pending,
Inactive,
Cancelled,
}
// 优化: 将最常用的值放在前面(0和1),因为较小的值编码成本更低
enum Status {
Active,
Pending,
Inactive,
Cancelled,
}
函数调用优化
减少外部调用:
ts
// 未优化: 多次外部调用
function processMultiStep() external {
externalContract.step1();
externalContract.step2();
externalContract.step3();
}
// 优化: 批量处理减少调用次数
function processMultiStep() external {
externalContract.processAll();
}
使用 internal 而非 public 函数:
ts
// 未优化: public函数增加参数验证和ABI编码成本
function helperFunction(uint256 x) public pure returns (uint256) {
return x * x;
}
// 优化: internal函数避免不必要的开销
function helperFunction(uint256 x) internal pure returns (uint256) {
return x * x;
}
避免函数参数过多:
ts
// 未优化: 多参数函数
function complexOperation(uint256 param1, uint256 param2, address param3, bytes memory param4, bool param5) external {
// 操作
}
// 优化: 使用结构体减少参数数量
struct OperationParams {
uint256 param1;
uint256 param2;
address param3;
bytes memory param4;
bool param5;
}
function complexOperation(OperationParams memory params) external {
// 操作
}
优化修饰器使用:
ts
// 未优化: 复杂修饰器带有额外逻辑
modifier complexCheck() {
require(condition1());
require(condition2());
require(condition3());
_;
}
// 优化: 使用函数而非修饰器处理复杂条件
function checkConditions() internal view {
require(condition1() && condition2() && condition3(), "Conditions not met");
}
// 在函数中调用: checkConditions();
错误处理优化
使用自定义错误替代字符串消息:
ts
// 未优化: 字符串错误消息占用更多存储空间
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, 'Insufficient balance');
// 转账逻辑
}
// 优化: 自定义错误更高效 (Solidity 0.8.4+)
error InsufficientBalance(address sender, uint256 balance, uint256 amount);
function transfer(address to, uint256 amount) external {
if (balances[msg.sender] < amount) {
revert InsufficientBalance(msg.sender, balances[msg.sender], amount);
}
// 转账逻辑
}
使用 if/revert 替代 require:
ts
// 未优化: require 包含字符串,占用更多 Gas
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, 'Insufficient balance');
// 提款逻辑
}
// 优化: if/revert 组合更高效
function withdraw(uint256 amount) external {
if (balances[msg.sender] < amount) revert();
// 提款逻辑
}
合并条件检查:
ts
// 未优化: 多个独立条件检查
function processTransaction(uint256 amount) external {
require(amount > 0, 'Amount must be positive');
require(amount <= maxAmount, 'Amount too large');
require(balances[msg.sender] >= amount, 'Insufficient balance');
// 处理交易
}
// 优化: 合并条件检查减少操作码
function processTransaction(uint256 amount) external {
require(amount > 0 && amount <= maxAmount && balances[msg.sender] >= amount, 'Invalid transaction');
// 处理交易
}
事件和日志优化
避免过多索引:
ts
// 未优化: 过多索引参数 (每个额外索引增加约400 Gas)
event Transfer(address indexed from, address indexed to, address indexed token, uint256 amount);
// 优化: 限制索引参数到必要字段
event Transfer(
address indexed from,
address indexed to,
address token, // 非索引
uint256 amount
);
批量事件:
ts
// 未优化: 每个操作都发出事件
function batchTransfer(address[] memory recipients, uint256[] memory amounts) external {
for (uint256 i = 0; i < recipients.length; i++) {
balances[msg.sender] -= amounts[i];
balances[recipients[i]] += amounts[i];
emit Transfer(msg.sender, recipients[i], amounts[i]);
}
}
// 优化: 为整批操作发出单个事件
function batchTransfer(address[] memory recipients, uint256[] memory amounts) external {
uint256 totalAmount = 0;
for (uint256 i = 0; i < recipients.length; i++) {
balances[msg.sender] -= amounts[i];
balances[recipients[i]] += amounts[i];
totalAmount += amounts[i];
}
emit BatchTransfer(msg.sender, recipients, amounts, totalAmount);
}
压缩事件数据:
ts
// 未优化: 包含冗余或可导出数据
event ComplexEvent(
address indexed user,
uint256 amount,
uint256 fee,
uint256 total, // 冗余,可从amount和fee计算
uint256 timestamp // 冗余,区块已包含时间戳
);
// 优化: 只包含必要数据
event StreamlinedEvent(address indexed user, uint256 amount, uint256 fee);
批量操作优化
实现批量转账:
ts
// 未优化: 单个转账,每次都需基础Gas成本
function transfer(address to, uint256 amount) external {
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
// 优化: 批量转账分摊固定成本
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
require(recipients.length == amounts.length, 'Length mismatch');
uint256 totalAmount = 0;
for (uint256 i = 0; i < recipients.length; i++) {
totalAmount += amounts[i];
}
require(balances[msg.sender] >= totalAmount, 'Insufficient balance');
balances[msg.sender] -= totalAmount;
for (uint256 i = 0; i < recipients.length; i++) {
balances[recipients[i]] += amounts[i];
emit Transfer(msg.sender, recipients[i], amounts[i]);
}
}
批量铸造和批量销毁:
ts
// 优化: NFT批量铸造
function batchMint(address to, uint256[] calldata tokenIds) external {
for (uint256 i = 0; i < tokenIds.length; i++) {
_mint(to, tokenIds[i]);
}
}
// 优化: 批量授权
function setApprovalForMany(address operator, uint256[] calldata tokenIds, bool approved) external {
for (uint256 i = 0; i < tokenIds.length; i++) {
tokenApprovals[tokenIds[i]] = approved ? operator : address(0);
emit Approval(ownerOf(tokenIds[i]), operator, tokenIds[i]);
}
}
汇编级优化
使用内联汇编优化存储操作:
ts
// 未优化: 标准Solidity存储读写
function incrementCounter() external {
counter += 1;
}
// 优化: 使用内联汇编直接操作存储
function incrementCounter() external {
assembly {
// 获取counter的存储槽
let counterSlot := counter.slot
// 从槽中加载值
let value := sload(counterSlot)
// 增加并存回
sstore(counterSlot, add(value, 1))
}
}
汇编优化字节数组处理:
ts
// 未优化: Solidity字节数组连接
function concatenate(bytes memory a, bytes memory b) public pure returns (bytes memory) {
return abi.encodePacked(a, b);
}
// 优化: 使用汇编高效连接字节数组
function concatenateAssembly(bytes memory a, bytes memory b) public pure returns (bytes memory) {
bytes memory result = new bytes(a.length + b.length);
assembly {
let len := mload(a)
mstore(add(result, 32), mload(a))
// 复制第一个数组
let dest := add(result, add(32, len))
let src := add(a, add(32, 0))
for {
let i := 0
} lt(i, len) {
i := add(i, 32)
} {
mstore(add(dest, i), mload(add(src, i)))
}
// 复制第二个数组
len := mload(b)
dest := add(result, add(32, mload(a)))
src := add(b, 32)
for {
let i := 0
} lt(i, len) {
i := add(i, 32)
} {
mstore(add(dest, i), mload(add(src, i)))
}
}
return result;
}
使用汇编进行高效签名验证:
ts
// 优化: 汇编实现的签名验证
function verifySignature(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
address signer;
assembly {
// ecrecover预编译合约地址为1
let memPtr := mload(0x40)
// 将参数放入内存
mstore(memPtr, hash)
mstore(add(memPtr, 32), v)
mstore(add(memPtr, 64), r)
mstore(add(memPtr, 96), s)
// 调用ecrecover预编译
let success := staticcall(gas(), 1, memPtr, 128, memPtr, 32)
// 检查调用是否成功
if iszero(success) {
revert(0, 0)
}
// 获取返回的地址
signer := mload(memPtr)
}
return signer;
}
合约架构优化
钻石模式 vs 代理模式:
- 代理模式: 通过委托调用实现可升级性,部署成本较低,但每次调用增加一定 Gas (DELEGATECALL)
- 钻石模式: 支持多面(facet),更模块化,适合复杂系统
- 选择取决于应用复杂性和预期升级频率
库合约的使用:
- 共享代码减少部署大小
- 使用内部库(internal libraries)让编译器内联代码,避免额外的DELEGATECALL成本
- 仅对多合约共享的复杂逻辑使用外部库
最小化合约部署成本:
- 移除不必要的功能
- 优化构造函数逻辑
- 考虑工厂合约模式批量部署
使用克隆工厂模式:
ts
// 优化部署多个类似合约的成本
contract MinimalProxy {
// 实现EIP-1167最小代理克隆
function clone(address implementation) internal returns (address instance) {
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(ptr, 0x14), shl(96, implementation))
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
instance := create(0, ptr, 0x37)
}
require(instance != address(0), 'Create failed');
}
}
接口重用:
ts
// 优化: 避免在多个合约中复制相同的接口定义
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
// 多个合约重用同一接口
contract Contract1 {
IERC20 public token;
// 使用接口
}
contract Contract2 {
IERC20 public token;
// 使用相同接口
}
编译器和工具优化
利用编译器优化器设置:
ts
// hardhat.config.js 示例
module.exports = {
solidity: {
version: '0.8.19',
settings: {
optimizer: {
enabled: true,
runs: 200, // 根据预期调用频率调整
details: {
yul: true, // 启用Yul优化器
yulDetails: {
stackAllocation: true, // 优化栈使用
optimizerSteps: 'dhfoDgvulfnTUtnIf', // 自定义优化步骤
},
},
},
},
},
}
- 较低的runs值优化部署 Gas
- 较高的runs值优化函数调用 Gas
- 根据预期调用频率选择合适的runs值
安全性与优化的平衡技巧
重用验证结果:
ts
// 未优化: 重复验证
function processMultiStep(uint256 value) external {
require(isAuthorized(msg.sender), 'Unauthorized');
step1(value);
require(isAuthorized(msg.sender), 'Unauthorized'); // 重复检查
step2(value);
}
// 优化: 缓存验证结果
function processMultiStep(uint256 value) external {
bool authorized = isAuthorized(msg.sender);
require(authorized, 'Unauthorized');
step1(value);
step2(value);
}
优雅降级:
ts
// 优化: 实现渐进式回退机制
function getData(uint256 id) public view returns (uint256) {
// 尝试从主数据源获取
try primarySource.getData(id) returns (uint256 value) {
return value;
} catch {
// 主数据源失败,回退到备用源
return backupSource.getData(id);
}
}
智能数据结构选择
使用紧凑位图替代布尔数组:
ts
// 未优化: 使用布尔数组存储标志
bool[] public flags;
// 优化: 使用位图存储多个布尔值
uint256 public flags;
function setFlag(uint8 index, bool value) public {
if (value) {
flags |= (1 << index); // 设置位
} else {
flags &= ~(1 << index); // 清除位
}
}
function getFlag(uint8 index) public view returns (bool) {
return (flags & (1 << index)) != 0;
}
使用枚举替代状态字符串:
ts
// 未优化: 字符串状态
string public currentState = "Active";
function changeState(string memory newState) public {
currentState = newState;
}
// 优化: 枚举状态
enum State { Active, Pending, Inactive }
State public currentState = State.Active;
function changeState(State newState) public {
currentState = newState;
}
使用链表处理频繁插入/删除:
ts
// 优化: 双向链表实现,适用于频繁插入/删除
struct Node {
uint256 id;
uint256 next;
uint256 prev;
}
uint256 public constant HEAD = 0;
uint256 public constant TAIL = 0;
mapping(uint256 => Node) public nodes;
uint256 public size;
function initialize() public {
// 创建哨兵节点
nodes[HEAD].next = TAIL;
nodes[TAIL].prev = HEAD;
}
function insertAfter(uint256 id, uint256 afterId) public {
uint256 beforeId = nodes[afterId].next;
nodes[id].prev = afterId;
nodes[id].next = beforeId;
nodes[afterId].next = id;
nodes[beforeId].prev = id;
size++;
}
function remove(uint256 id) public {
uint256 prevId = nodes[id].prev;
uint256 nextId = nodes[id].next;
nodes[prevId].next = nextId;
nodes[nextId].prev = prevId;
delete nodes[id];
size--;
}
访问控制优化
使用标志位控制权限:
ts
// 未优化: 多个独立角色标志
mapping(address => bool) public isAdmin;
mapping(address => bool) public isOperator;
mapping(address => bool) public isAuditor;
// 优化: 位标志角色系统
uint8 public constant ROLE_ADMIN = 1; // 0001
uint8 public constant ROLE_OPERATOR = 2; // 0010
uint8 public constant ROLE_AUDITOR = 4; // 0100
mapping(address => uint8) public userRoles;
function grantRole(address user, uint8 role) public {
userRoles[user] |= role;
}
function revokeRole(address user, uint8 role) public {
userRoles[user] &= ~role;
}
function hasRole(address user, uint8 role) public view returns (bool) {
return (userRoles[user] & role) != 0;
}
优化授权检查顺序:
ts
// 未优化: 所有条件同时检查
function executeAction() public {
require(isAdmin[msg.sender] || isOperator[msg.sender] || msg.sender == owner, 'Unauthorized');
// 执行操作
}
// 优化: 按条件检查概率排序,最可能成功的先检查
function executeAction() public {
// 假设操作者调用最频繁
if (isOperator[msg.sender]) {
// 执行操作
return;
}
// 其次是管理员
if (isAdmin[msg.sender]) {
// 执行操作
return;
}
// 最后检查所有者
if (msg.sender == owner) {
// 执行操作
return;
}
revert('Unauthorized');
}
时间管理优化
使用相对时间而非绝对时间戳:
ts
// 未优化: 存储绝对时间戳
mapping(address => uint256) public lockUntil;
function lock(uint256 durationSeconds) public {
lockUntil[msg.sender] = block.timestamp + durationSeconds;
}
// 优化: 存储相对于区块的增量
uint256 public immutable genesisBlock;
mapping(address => uint256) public lockDuration;
constructor() {
genesisBlock = block.number;
}
function lock(uint256 blockCount) public {
// 假设每15秒一个区块
lockDuration[msg.sender] = blockCount;
}
function isLocked(address user) public view returns (bool) {
return block.number < genesisBlock + lockDuration[user];
}
批量更新时间状态:
ts
// 未优化: 为每个用户存储时间戳
mapping(address => uint256) public lastActionTime;
function updateLastAction() public {
lastActionTime[msg.sender] = block.timestamp;
}
// 优化: 使用批次ID标记时间段
uint256 public currentBatchId;
uint256 public lastBatchUpdate;
mapping(address => uint256) public userLastBatchId;
function updateBatch() public {
if (block.timestamp - lastBatchUpdate > 1 hours) {
currentBatchId++;
lastBatchUpdate = block.timestamp;
}
}
function updateUserBatch(address user) public {
userLastBatchId[user] = currentBatchId;
}
状态管理优化
状态压缩:
ts
// 未优化: 多个状态变量
bool public isPaused;
bool public isUpgrading;
bool public isEmergency;
uint8 public currentVersion;
// 优化: 压缩状态到单个uint256
// 位 0: isPaused
// 位 1: isUpgrading
// 位 2: isEmergency
// 位 8-15: currentVersion (8位)
uint256 public packedState;
function isPaused() public view returns (bool) {
return (packedState & 1) == 1;
}
function isUpgrading() public view returns (bool) {
return (packedState & 2) == 2;
}
function isEmergency() public view returns (bool) {
return (packedState & 4) == 4;
}
function currentVersion() public view returns (uint8) {
return uint8((packedState >> 8) & 0xFF);
}
function setVersion(uint8 version) public {
// 清除旧版本位并设置新版本
packedState = (packedState & ~(0xFF << 8)) | (uint256(version) << 8);
}
惰性删除和惰性更新:
ts
// 未优化: 立即删除
mapping(uint256 => Item) public items;
uint256[] public activeItemIds;
function removeItem(uint256 itemId) public {
// 从映射中删除
delete items[itemId];
// 从数组中删除(昂贵)
for (uint256 i = 0; i < activeItemIds.length; i++) {
if (activeItemIds[i] == itemId) {
// 移动元素以保持数组连续
activeItemIds[i] = activeItemIds[activeItemIds.length - 1];
activeItemIds.pop();
break;
}
}
}
// 优化: 惰性删除
mapping(uint256 => Item) public items;
mapping(uint256 => bool) public isDeleted;
function markItemDeleted(uint256 itemId) public {
isDeleted[itemId] = true;
}
function getActiveItem(uint256 itemId) public view returns (Item memory) {
require(!isDeleted[itemId], "Item deleted");
return items[itemId];
}
小结
虽然上面列了很多 Gas 的优化手段,但是不是所有的优化手段都得用上,还得考虑代码可读性,我们可以优先考虑优化收益最大的存储操作。
总结
让我帮您优化小结和总结部分,使其更有价值和实用性:
小结
在实际开发中,Gas 优化需要权衡多个因素:
-
优化优先级
- 存储操作(SSTORE/SLOAD)优化收益最大
- 外部调用(CALL/DELEGATECALL)次数优化其次
- 计算操作优化收益相对较小
-
可维护性平衡
- 过度优化可能导致代码难以理解和维护
- 建议先保证代码清晰可读,再进行必要的优化
- 关键路径和热点函数优先优化
-
优化成本评估
- 评估优化投入与收益比
- 考虑合约调用频率
- 权衡开发时间成本
总结
本文详细介绍了以太坊智能合约的 Gas 优化策略。在实际应用中,建议:
-
开发阶段
- 培养 Gas 优化意识,在编码时注意避免高消耗操作
- 使用 Hardhat 或 Foundry 等工具实时监控 Gas 消耗
- 建立团队的 Gas 优化规范和检查清单
-
测试阶段
- 进行全面的 Gas 消耗测试
- 对比优化前后的 Gas 差异
- 验证优化后的代码正确性
-
部署后
- 持续监控主网上的实际 Gas 消耗
- 收集用户反馈,识别潜在的优化空间
- 在必要时进行合约升级优化
记住,Gas 优化是一个持续改进的过程,需要在效率、安全性和可维护性之间找到适当的平衡点。
参考资料
以太坊官方文档与提案
- 以太坊黄皮书 - 以太坊技术规范,包含EVM操作码及Gas计算详细说明
- EIP-2929: 状态访问操作码Gas成本增加 - 热访问与冷访问机制详细说明
- EIP-1559: 燃料费市场改革 - 基础费用与小费机制详解
- EIP-3529: 减少退款上限 - 关于Gas退款机制的变更
- EIP-4844: Proto-Danksharding - Blob交易与Layer 2扩容方案
- EIP-2930: 可选访问列表 - 预声明访问列表以降低Gas成本
Gas优化工具与资源
- Remix IDE - 带Gas估算功能的智能合约开发环境
- Hardhat Gas Reporter - Hardhat框架的Gas分析工具
- Foundry Gas Report - Foundry框架的Gas报告功能
- OpenZeppelin合约库 - 优化的标准合约实现
- Etherscan Gas Tracker - 实时以太坊Gas价格跟踪