本文探讨了区块链开发中gas优化的各种策略,通过代码示例,将优化方法归为架构级优化(合约设计模式)、代码级优化(开发者控制)和部署优化三个类别。内容涉及变量类型选择与布局、存储设计、函数设计、逻辑与循环优化、事件与错误处理、工具与测试、以及汇编层面的优化技巧,旨在降低gas消耗,提高合约执行效率和用户体验。
在区块链开发中,Gas 是用于衡量执行交易和智能合约成本的单位。每个操作——无论是存储、计算还是数据传输——都会消耗一定量的 Gas,用户在发送交易时必须支付相应的费用。因此,Gas 优化不仅降低了用户的交易成本,还提高了合约执行效率,增强了用户体验。
本文通过实际的代码示例探讨各种 Gas 优化策略。优化方法大致可分为三类:架构级别的优化(合约设计模式)→ 代码级别的优化(开发者可控)→ 部署优化。
尽可能使用 256 位的数据类型,因为 EVM 以 256 位槽(32 字节)为单位读取存储。
contract A {
uint8 public a = 0;
function setA(uint8 _a) public {
a = _a;
}
}
contract B {
uint256 public a = 0;
function setA(uint256 _a) public {
a = _a;
}
}
测试表明,无论是部署还是执行读/写操作,合约 B 比合约 A 消耗的 Gas 更少。
由于 EVM 的紧密打包机制,在 struct 中使用较小的数据类型可以节省 Gas,因为它们会被打包到同一个存储槽中。
contract A {
struct Unpacked {
uint256 x;
uint256 y;
uint256 z;
}
}
contract B {
struct Packed {
uint64 x;
uint64 y;
uint128 z;
}
}
紧密打包的同样概念适用:如果一个操作同时访问多个存储变量,你可以尽可能将它们打包到一个槽中。例如,你可以使用 uint128
而不是 uint256
来表示时间。
uint128 startTime;
uint128 endTime;
function setTime(uint256 _startTime, uint256 _endTime) external {
// ...
}
如果一个状态变量不需要从外部访问,将其标记为 private
而不是 public
。
contract A {
uint256 public a;
uint256 b;
function setA(uint8 _a) public {
a = _a;
}
function setB(uint256 _b) public {
b = _b;
}
}
变量在存储中的排列顺序会影响插槽的利用率。尝试将小数据类型(如 uint8
和 bool
)组合在同一个存储槽中。
contract A {
struct UnOrdered {
uint64 x;
uint256 y;
address z;
}
}
contract B {
struct Ordered {
uint64 x;
address y;
uint256 z;
}
}
使用 constant
和 immutable
关键字可以帮助减少 Gas 消耗。
contract GasOptimized {
uint256 public constant TAX_RATE = 10;
address public immutable FIX;
}
在 Solidity ^0.8.24 及以上版本中,你可以利用瞬态存储来节省 Gas,因为它避免了存储访问。但是,请注意潜在的风险。例如,在修饰符中:
modifier nonreentrant {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
_;
// 解锁保护,以便该模式可组合。
// 函数退出后,即使在同一事务中也可以再次调用。
assembly {
tstore(0, 0)
}
}
在 Solidity ^0.8.27 之后,支持 transient
关键字。
正如以太坊黄皮书中所述:
contract A {
uint256 a;
function setData(uint256 _data) public {
a = _data;
}
}
contract B {
uint256 a = 1;
function setData(uint256 _data) public {
a = _data;
}
}
其他技巧包括:
uint256(1)
/uint256(2)
而不是 false
/true
。n
迭代到 0
,而不是从 0
迭代到 n
。首次访问存储变量的成本为 G_coldload
(2100 Gas),而后续访问的成本为 G_warmaccess
(100 Gas)。因此,在进入循环之前将存储变量复制到内存中,以避免重复的昂贵读取。
function sumInefficient() public view returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < number; i++) {
total += number;
}
return total;
}
function sumEfficient() public view returns (uint256) {
uint256 total = 0;
uint256 len = number;
for (uint256 i = 0; i < len; i++) {
total += number;
}
return total;
例如,当数组长度为 10 时,这可能会导致显着的 Gas 节省。
如果不再需要某个变量,删除它可以获得 Gas 退款。
contract UnusedVariables {
uint256 public data;
function useDelete() public {
data = 123; // 示例操作
delete data; // 将数据重置为其默认值
}
function skipDelete() public {
data = 123; // 没有删除的示例操作
}
}
当函数参数在执行期间不需要修改时,首选使用 calldata
而不是 memory
。
function loop_memory(uint256[] memory arr) external pure returns (uint256 sum) {
for (uint256 i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
function loop_calldata(uint256[] calldata arr) external pure returns (uint256 sum) {
for (uint256 i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
明确指定函数的可见性可以减少 Gas 的使用。与 public
函数相比,标记为 external
的函数对于外部调用来说更有效率。此外,使用 view
和 pure
告诉编译器该函数不修改状态,从而实现进一步的优化。
// Public 函数示例
function publicFunction(uint256[100] calldata data) public pure returns (uint256) {
return data[0];
}
// External 函数示例
function externalFunction(uint256[100] calldata data) external pure returns (uint256) {
return data[0];
}
calldata
读取,避免了将数据复制到内存中的成本。Solidity 编译器按接口选择器(4 个字节)对所有函数进行排序,并在函数调用期间迭代每个函数(每次迭代的成本为 22 Gas)。因此,频繁调用的函数最好具有较小的选择器值。例如:
red() => 2930cf24 // red() 选择器
white() => a0811074 // white() 选择器
yellow() => be9faf13 // yellow() 选择器
blue() => ed18f0a7 // blue() 选择器
purple() => ed44cd44 // purple() 选择器
green() => f2f1e132 // green() 选择器
与调用 red()
相比,调用 green()
将额外花费 110 Gas (5×22)。
复杂的条件语句会增加 Gas 的使用。简化逻辑并使用短路求值(&&
和 ||
)以避免不必要的计算。
// 情况 1:花费 100 Gas
if(case2 && case1) revert;
// 情况 2:花费 50 Gas
if(case2 || case1) revert;
此示例演示了条件检查中的短路如何减少 Gas 消耗。
在循环中利用 unchecked
块可以节省溢出检查的 Gas:
// 非优化的标准循环
for (uint i = 0; i < arr.length; i++) {
// 自动溢出检查
}
// 使用 unchecked 优化
for (uint i = 0; i < arr.length; ) {
// 循环体...
unchecked { i++; } // 通过省略溢出检查来节省 Gas
}
注意: 由于 Solidity v0.8.22,编译器在 for 循环中引入了 unchecked 优化,因此手动添加 unchecked 常常是不必要的。
例如,在 UniswapV2 中,在继续进行进一步操作之前,检查 amount
是否大于零,可以避免当值为零时的不必要传输:
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // 乐观地转移 Token
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // 乐观地转移 Token
声明一个没有初始化的状态变量允许 EVM 在部署时自动分配其默认值,与显式初始化相比,可以节省 Gas:
uint256 number; // 花费较少
uint256 number = 0; // 花费较多
bool claim; // 花费较少
bool claim = false; // 花费较多
address owner; // 花费较少
address owner = address(0); // 花费较多
与依赖带有错误字符串的 require
语句相比,使用自定义错误可以更有效地利用 Gas。
// 使用 require 的非优化方式
function withdraw(uint amount) public {
require(balance >= amount, "Insufficient balance"); // 更多 Gas 消耗
}
// 使用自定义错误进行优化
error InsufficientBalance(uint available, uint required);
function withdrawOptimized(uint amount) public {
if (balance < amount)
revert InsufficientBalance(balance, amount); // 节省 Gas
}
使用 Gas 分析工具(例如 Remix、Truffle、Hardhat 等)来测试和识别合约中 Gas 消耗高的区域。定期监控 Gas 使用情况并根据需要进行优化。
在 Solidity 编译器设置中启用优化器:
settings: {
optimizer: {
enabled: true,
runs: 200 // 合约执行次数
}
}
contract AssemblyExample {
function add(uint256 a, uint256 b) public pure returns (uint256 result) {
assembly {
result := add(a, b)
}
}
}
常见的操作,例如 ERC20/ERC721 中的操作(如 approve
、safeTransferFrom
)或外部调用(call
/delegateCall
),可以用汇编重写以进一步节省 Gas。
Gas 优化是 Solidity 智能合约开发的一个关键方面。通过仔细选择数据类型、优化控制结构、减少外部调用和选择高效的算法,开发人员可以显着降低 Gas 消耗,从而提高合约执行效率和用户体验。持续监控和定期优化你的合约对于实现最佳性能至关重要。
Solidity Gas 优化文章的翻译和 Medium 格式版本到此结束。祝你优化合约愉快!
- 原文链接: blog.blockmagnates.com/s...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!