在进行 gas 优化之前,首先需要准确地测量和评估代码的 gas 消耗。本章将介绍几种常用的 gas 评估方法,帮助你识别优化点并验证优化效果。
Gas 评估是优化工作的基础:
Foundry 提供了强大的 gas 报告功能,可以自动统计测试中每个函数的 gas 消耗。
在运行测试时添加 --gas-report 标志:
forge test --gas-report
输出示例:
| src/MyToken.sol:MyToken contract | | | | | |
|----------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 500000 | 2500 | | | | |
| Function Name | min | avg | median | max | # calls |
| transfer | 51343 | 51343 | 51343 | 51343 | 1 |
| mint | 48000 | 48000 | 48000 | 48000 | 2 |
| approve | 46000 | 46000 | 46000 | 46000 | 1 |
在 foundry.toml 中配置 gas 报告选项:
[profile.default]
gas_reports = ["*"] # 报告所有合约
# gas_reports = ["MyToken", "MyNFT"] # 只报告特定合约
# 排除某些合约
gas_reports_ignore = ["MockContract", "TestHelpers"]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
address user1 = address(0x1);
address user2 = address(0x2);
function setUp() public {
token = new MyToken();
token.mint(user1, 1000 ether);
}
// 测试 transfer 的 gas 消耗
function testTransferGas() public {
vm.prank(user1);
token.transfer(user2, 100 ether);
}
// 测试 approve 的 gas 消耗
function testApproveGas() public {
vm.prank(user1);
token.approve(user2, 100 ether);
}
// 对比不同场景的 gas 消耗
function testFirstTransferVsSubsequent() public {
// 首次转账(冷存储 -> 热存储)
vm.prank(user1);
token.transfer(user2, 100 ether);
// 后续转账(热存储 -> 热存储)
vm.prank(user1);
token.transfer(user2, 100 ether);
}
}
运行测试:
forge test --gas-report --match-contract MyTokenTest
✅ 自动化 - 无需修改合约代码 ✅ 统计全面 - 显示 min/avg/median/max 值 ✅ 易于对比 - 直观展示所有函数的 gas 消耗
Gas Snapshots 是 Foundry 的另一个强大功能,用于跟踪 gas 消耗的变化。
首次运行以下命令创建 gas 快照:
forge snapshot
forge snapshot --snap <FILE_NAME> # 自定义的gas 快照文件名
默认会在项目根目录生成一个 .gas-snapshot 文件,记录每个测试的 gas 消耗:
MyTokenTest:testApproveGas() (gas: 46123)
MyTokenTest:testFirstTransferVsSubsequent() (gas: 102456)
MyTokenTest:testTransferGas() (gas: 51343)
修改代码后,再次运行 forge snapshot,Foundry 会自动对比并显示差异:
forge snapshot --diff
输出示例:
MyTokenTest:testTransferGas() (gas: 51243 (↓ -100 | -0.19%))
MyTokenTest:testApproveGas() (gas: 46123 (no change))
MyTokenTest:testMintGas() (gas: 48150 (↑ +150 | +0.31%))
使用 --check 标志,如果 gas 消耗增加超过阈值则失败:
forge snapshot --check
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
contract GasOptimizationTest is Test {
// 测试优化前的版本
function testUnoptimized() public {
// 未优化的实现
uint256 sum;
for (uint256 i = 0; i < 100; i++) {
sum += i;
}
}
// 测试优化后的版本
function testOptimized() public {
// 优化后的实现
uint256 sum;
for (uint256 i; i < 100;) {
sum += i;
unchecked { ++i; }
}
}
}
运行并对比:
forge snapshot --match-test "test(Un)?[Oo]ptimized"
✅ 提交到版本控制 - 将 .gas-snapshot 文件提交到 Git
✅ 在 PR 中审查 - 关注 gas 消耗的变化
✅ 设置告警阈值 - 显著的 gas 增加应该引起注意
✅ 为关键函数编写专门测试 - 确保核心功能的 gas 效率
gasleft() 是 Solidity 内置函数,返回当前剩余的 gas 数量,可以用来精确测量特定代码段的 gas 消耗。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract GasMeter {
event GasConsumed(string operation, uint256 gasUsed);
function measureGas() public {
uint256 gasBefore = gasleft();
// 要测量的操作
uint256 result = expensiveOperation();
uint256 gasAfter = gasleft();
uint256 gasUsed = gasBefore - gasAfter;
emit GasConsumed("expensiveOperation", gasUsed);
}
function expensiveOperation() internal pure returns (uint256) {
uint256 sum;
for (uint256 i = 0; i < 100; i++) {
sum += i * i;
}
return sum;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
library GasProfiler {
struct Checkpoint {
uint256 gasLeft;
string label;
}
/// @notice 开始 gas 测量
function start() internal view returns (uint256) {
return gasleft();
}
/// @notice 结束 gas 测量并返回消耗的 gas
function end(uint256 startGas) internal view returns (uint256) {
return startGas - gasleft();
}
/// @notice 测量代码块的 gas 消耗
function measure(
string memory label
) internal view returns (uint256 gasUsed) {
uint256 gasBefore = gasleft();
// 注意:这个函数返回 gasBefore,需要在外部计算 gasUsed
return gasBefore;
}
}
contract UsingGasProfiler {
using GasProfiler for *;
event GasReport(string label, uint256 gasUsed);
function complexOperation() public {
// 测量存储写入
uint256 g1 = GasProfiler.start();
storageWrite();
emit GasReport("Storage Write", GasProfiler.end(g1));
// 测量内存操作
uint256 g2 = GasProfiler.start();
memoryOperation();
emit GasReport("Memory Operation", GasProfiler.end(g2));
// 测量循环
uint256 g3 = GasProfiler.start();
loopOperation();
emit GasReport("Loop Operation", GasProfiler.end(g3));
}
uint256 public data;
function storageWrite() internal {
data = 12345;
}
function memoryOperation() internal pure {
uint256[] memory arr = new uint256[](100);
for (uint256 i = 0; i < 100; i++) {
arr[i] = i;
}
}
function loopOperation() internal pure {
uint256 sum;
for (uint256 i = 0; i < 100; i++) {
sum += i;
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
contract GasComparisonTest is Test {
function testCompareImplementations() public {
// 测量实现 A
uint256 gasBeforeA = gasleft();
uint256 resultA = implementationA();
uint256 gasUsedA = gasBeforeA - gasleft();
// 测量实现 B
uint256 gasBeforeB = gasleft();
uint256 resultB = implementationB();
uint256 gasUsedB = gasBeforeB - gasleft();
// 验证结果相同
assertEq(resultA, resultB, "Results should be equal");
// 输出 gas 对比
console.log("Implementation A gas:", gasUsedA);
console.log("Implementation B gas:", gasUsedB);
console.log("Gas saved:", gasUsedA > gasUsedB ? gasUsedA - gasUsedB : 0);
// 断言优化版本更省 gas
assertLt(gasUsedB, gasUsedA, "Implementation B should use less gas");
}
function implementationA() internal pure returns (uint256) {
uint256 sum;
for (uint256 i = 0; i < 100; i++) {
sum = sum + i;
}
return sum;
}
function implementationB() internal pure returns (uint256) {
uint256 sum;
for (uint256 i = 0; i < 100;) {
sum = sum + i;
unchecked { ++i; }
}
return sum;
}
}
⚠️ 测量开销 - gasleft() 本身消耗约 2 gas,在精确测量时需要考虑
⚠️ 编译器优化 - 某些情况下编译器可能会优化掉未使用的代码
⚠️ 外部调用影响 - 跨合约调用时,gas 的计算可能更复杂
⚠️ Context 依赖 - 测量结果可能受到调用上下文的影响(如存储槽的冷/热状态)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract AdvancedGasMeasurement {
// 测量多个操作并返回详细报告
struct GasReport {
uint256 operation1Gas;
uint256 operation2Gas;
uint256 operation3Gas;
uint256 totalGas;
}
function detailedGasReport() public view returns (GasReport memory) {
uint256 totalStart = gasleft();
uint256 g1 = gasleft();
// 操作 1
uint256 temp1 = heavyComputation(50);
uint256 op1Gas = g1 - gasleft();
uint256 g2 = gasleft();
// 操作 2
uint256 temp2 = heavyComputation(100);
uint256 op2Gas = g2 - gasleft();
uint256 g3 = gasleft();
// 操作 3
uint256 temp3 = heavyComputation(150);
uint256 op3Gas = g3 - gasleft();
uint256 totalGas = totalStart - gasleft();
return GasReport({
operation1Gas: op1Gas,
operation2Gas: op2Gas,
operation3Gas: op3Gas,
totalGas: totalGas
});
}
function heavyComputation(uint256 n) internal pure returns (uint256) {
uint256 result;
for (uint256 i = 0; i < n; i++) {
result += i ** 2;
}
return result;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
// 待优化的合约
contract TokenV1 {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] = balances[msg.sender] - amount;
balances[to] = balances[to] + amount;
}
}
// 优化后的合约
contract TokenV2 {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
unchecked {
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
}
// 完整的测试和评估
contract GasComparisonTest is Test {
TokenV1 tokenV1;
TokenV2 tokenV2;
address user1 = address(0x1);
address user2 = address(0x2);
function setUp() public {
tokenV1 = new TokenV1();
tokenV2 = new TokenV2();
// 初始化余额
vm.store(
address(tokenV1),
keccak256(abi.encode(user1, 0)),
bytes32(uint256(1000 ether))
);
vm.store(
address(tokenV2),
keccak256(abi.encode(user1, 0)),
bytes32(uint256(1000 ether))
);
}
// 方法 1: 使用 forge test --gas-report
function testV1Transfer() public {
vm.prank(user1);
tokenV1.transfer(user2, 100 ether);
}
function testV2Transfer() public {
vm.prank(user1);
tokenV2.transfer(user2, 100 ether);
}
// 方法 2: 使用 gasleft() 精确对比
function testGasComparison() public {
// 测量 V1
vm.prank(user1);
uint256 gasBeforeV1 = gasleft();
tokenV1.transfer(user2, 100 ether);
uint256 gasUsedV1 = gasBeforeV1 - gasleft();
// 重置状态
setUp();
// 测量 V2
vm.prank(user1);
uint256 gasBeforeV2 = gasleft();
tokenV2.transfer(user2, 100 ether);
uint256 gasUsedV2 = gasBeforeV2 - gasleft();
// 输出对比
console.log("V1 Gas Used:", gasUsedV1);
console.log("V2 Gas Used:", gasUsedV2);
console.log("Gas Saved:", gasUsedV1 - gasUsedV2);
console.log(
"Improvement:",
(gasUsedV1 - gasUsedV2) * 100 / gasUsedV1,
"%"
);
// 断言优化有效
assertLt(gasUsedV2, gasUsedV1, "V2 should be more gas efficient");
}
}
# 1. 生成 gas report
forge test --gas-report --match-contract GasComparisonTest
# 2. 创建 snapshot
forge snapshot --match-contract GasComparisonTest
# 3. 查看详细输出
forge test --match-test testGasComparison -vv
Gas 评估是优化工作的基础,本章介绍了三种主要的评估方法:
| 方法 | 优点 | 适用场景 |
|---|---|---|
| Forge Gas Report | 自动化、全面 | 整体性能评估 |
| Gas Snapshots | 追踪变化、CI 集成 | 防止性能退化 |
| gasleft() | 精确、灵活 | 细粒度测量 |
推荐做法是:
.gas-snapshot 文件掌握这些工具后,你就可以准确识别优化点,验证优化效果,并在开发过程中持续监控 gas 效率。
下一章我们将开始学习具体的 gas 优化技巧。