如何估算和优化 Gas

在以太坊里,Gas 是每个人必须理解的核心概念。本文主要讨论如何估算和优化 Gas,帮助开发者们能够写出更节能的区块链应用。

Gas 是什么

Gas 是以太坊里用来衡量计算资源消耗的单位。在以太坊上执行写操作(例如转账)都得需要消耗一定数量的 Gas,读操作(例如查询余额)一般不需要消耗 Gas。每次写操作消耗的 Gas 费用为:Gas 数量 * Gas 价格 = 交易费用。这种机制设计有两个核心目的:

  1. 防止网络滥用:通过为每个写操作设置成本,防止恶意行为者通过执行无限循环或资源密集型操作来攻击网络。
  2. 激励验证者:为网络维护者提供经济激励,补偿他们验证交易和执行计算所花费的资源。

简而言之,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 消耗如下所示:

  1. 普通 ETH 转账交易基础费用:21000 Gas

  2. 余额检查:

    • SLOAD 读取 _balances[msg.sender] (冷访问): 2100 Gas
    • 比较操作 (<): 3 Gas
  3. 余额更新:

    • SLOAD 读取 _balances[msg.sender] (热访问): 100 Gas
    • SSTORE 更新 _balances[msg.sender]: 5000 Gas
    • SLOAD 读取 _balances[recipient] (冷访问): 2100 Gas
    • SSTORE 更新 _balances[recipient] (假设首次写入): 20000 Gas
  4. 事件发送:

    • LOG3 (Transfer 事件): ~1500 Gas
  5. 其他开销:

    • 函数调用和返回: ~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 优化需要权衡多个因素:

  1. 优化优先级

    • 存储操作(SSTORE/SLOAD)优化收益最大
    • 外部调用(CALL/DELEGATECALL)次数优化其次
    • 计算操作优化收益相对较小
  2. 可维护性平衡

    • 过度优化可能导致代码难以理解和维护
    • 建议先保证代码清晰可读,再进行必要的优化
    • 关键路径和热点函数优先优化
  3. 优化成本评估

    • 评估优化投入与收益比
    • 考虑合约调用频率
    • 权衡开发时间成本

总结

本文详细介绍了以太坊智能合约的 Gas 优化策略。在实际应用中,建议:

  1. 开发阶段

    • 培养 Gas 优化意识,在编码时注意避免高消耗操作
    • 使用 Hardhat 或 Foundry 等工具实时监控 Gas 消耗
    • 建立团队的 Gas 优化规范和检查清单
  2. 测试阶段

    • 进行全面的 Gas 消耗测试
    • 对比优化前后的 Gas 差异
    • 验证优化后的代码正确性
  3. 部署后

    • 持续监控主网上的实际 Gas 消耗
    • 收集用户反馈,识别潜在的优化空间
    • 在必要时进行合约升级优化

记住,Gas 优化是一个持续改进的过程,需要在效率、安全性和可维护性之间找到适当的平衡点。

参考资料

以太坊官方文档与提案

Gas优化工具与资源

相关推荐
秋说3 小时前
【区块链安全 | 第十三篇】Solidity 合约结构
安全·区块链
秋说4 小时前
【区块链安全 | 第十四篇】类型之值类型(一)
安全·区块链
倒霉男孩4 小时前
NFT (Non-Fungible Token)
区块链
秋说6 小时前
【区块链安全 | 第十二篇】安装 Solidity 编译器(npm | docker | Linux | macOS)
安全·npm·区块链
人类群星闪耀时7 小时前
区块链与AI的融合:技术交汇的未来之路
人工智能·区块链
奶油话梅糖14 小时前
区块链技术详解:应用场景与核心优势
区块链
搬砖魁首1 天前
2024年零知识证明(ZK)研究进展
区块链·零知识证明·zk·sumcheck·sum-check
人类群星闪耀时1 天前
区块链在教育领域的创新应用:改变传统教育的未来
区块链
Blockchina1 天前
第 4 章 | Solidity安全 权限控制漏洞全解析
安全·web3·区块链·智能合约·solidity