本文深入探讨了Solidity智能合约中的Gas优化技术。文章从Gas成本的基本概念出发,详细阐述了存储、内存、Calldata的区别,并提供了变量打包、存储访问优化、函数级别优化、循环效率提升以及选择合适数据结构等多种实用技巧。此外,文章还介绍了使用内联汇编和位运算等高级优化策略,旨在帮助开发者编写更高效、更经济的合约。
2026年3月2日
以太坊网络上的每笔交易都会产生费用——gas。你的智能合约效率越高,用户与它交互需要支付的 gas 就越少。Gas 是执行操作所需的费用,无论这些操作涉及转移 ETH、与智能合约交互还是执行链上计算。操作越复杂,消耗的 gas 越多。如果你的智能合约没有经过优化,用户可能最终支付不必要的费用,在某些情况下,交易可能因区块 gas 限制而失败。
随着 Polygon 和 Arbitrum 等 Layer 2 解决方案的兴起,一些人可能认为 gas 优化不再那么重要。然而,这些解决方案仍然运行在以太坊的基础之上并产生费用。Layer 2 网络上的 gas 费用可能较低,但它们并非微不足道,尤其是在采用率增加和网络需求增长的情况下。此外,以太坊的区块 gas 限制(通常在3000万 gas 单位左右)对交易执行施加了限制。优化不佳的合约可能会触及这些限制,导致交易失败和 gas 费用浪费。
对于开发者而言,编写高效的 Solidity 代码不仅仅是为了节省成本,更是为了提升可用性。一个消耗过多 gas 的合约会阻碍用户互动,使其相较于更优化的替代方案吸引力不足。本指南探讨了在 Solidity 中降低 gas 成本的实用方法,确保你的合约平稳运行且不会让你破产。
Solidity 提供了三种主要的数据存储方式:
在传递参数时,尽可能使用 calldata 而不是 memory,以降低 gas 成本。
并非所有操作都消耗相同数量的 gas。有些操作比其他操作昂贵得多:
了解哪些操作消耗的 gas 最多有助于你编写更高效的合约。
Solidity 将数据存储在256位的插槽中。如果你使用像 uint128 这样较小的类型,你可以将两个值放入一个插槽中,从而降低 gas 成本。Solidity 编译器和优化器会自动处理打包;你只需在合约中连续声明可打包的变量即可。
contract OptimizedStorage {
uint128 a;
uint128 b; // 与 `a` 共享一个存储槽
uint256 c; // 需要一个单独的槽
}
如果 c 放在 a 和 b 之间,Solidity 将使用额外的存储槽,不必要地增加了 gas 成本。
Solidity 提供了多种整数大小(uint8、uint16、uint256)。虽然使用较小类型似乎总是能节省 gas,但这仅在变量被打包时才成立。否则,使用 uint256 通常更高效,因为 EVM 原生处理256位字。
写入存储是 Solidity 中最昂贵的操作之一。与其频繁修改存储的变量,不如在内存中计算重复的值,然后只将最终结果一次性写入存储。
contract GasSaver {
uint256 public total;
function add(uint256[] calldata values) external {
uint256 sum;
for (uint i = 0; i < values.length; i++) {
sum += values[i];
}
total = sum; // 单次存储写入
}
}
通过首先在内存中计算总和,然后只进行一次存储写入,这种方法显著降低了 gas 成本。
在不再需要存储变量时清除它们可以为你赢得 gas 退款。
delete myVariable; // 触发 gas 退款
旨在进行外部调用的函数应标记为 external 而不是 public。这可以防止 Solidity 将函数参数复制到内存中,从而降低 gas 成本。
contract EfficientFunctions {
function process(uint256 data) external returns (uint256) {
return data * 2;
}
}
如果一个值在一个函数中需要多次使用,请将其存储在局部变量中,而不是重复计算。
效率低下(不必要的重复计算)
function calculate(uint256 a, uint256 b) external pure returns (uint256) {
return (a * b) + (a * b) + (a * b);
}
在这种情况下,乘法 a * b 执行了三次,增加了 gas 使用量。
优化示例(一次存储计算结果)
function calculate(uint256 a, uint256 b) external pure returns (uint256) {
uint256 product = a * b;
return product + product + product;
}
现在,乘法只执行一次,结果被复用,从而节省了 gas。
循环可能代价高昂,尤其是在处理大型数组时。通过只获取一次数组长度而不是在循环内部多次调用 .length 来避免不必要的迭代。
function processArray(uint256[] calldata data) external {
uint256 length = data.length;
for (uint256 i = 0; i < length; i++) {
// 处理数据
}
}
算术运算,如加法、减法和乘法,通常包含溢出检查,以确保结果保持在数据类型(例如 uint256)的有效值范围内。如果发生溢出,Solidity 会自动回滚交易以防止意外行为。
然而,这些检查会消耗额外的 gas,因为 EVM 需要执行比较以确保不会发生溢出。如果你确信某个操作不会溢出(例如,当你知道所涉及的数字足够小),你可以使用 unchecked 关键字跳过这些溢出检查,这可以节省 gas。
为什么 unchecked 能够节省 Gas?
unchecked,Solidity 不需要对每个操作执行溢出检查。结果是它跳过了验证操作是否在界限内的计算开销,从而略微降低了这些操作的 gas 消耗。unchecked 允许你使这些操作更便宜,因为没有 gas 用于执行溢出检查。示例:
以下是 unchecked 在简单加法操作中的使用示例:
pragma solidity ^0.8.0;
contract GasOptimization {
function safeAdd(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // 带有溢出检查的正常加法
}
function uncheckedAdd(uint256 a, uint256 b) public pure returns (uint256) {
unchecked {
return a + b; // 没有溢出检查的加法
}
}
}
在 uncheckedAdd 函数中,加法是在没有检查结果是否溢出的情况下完成的。这跳过了内部安全检查,可以节省 gas,特别是当你确定值不会导致溢出时。
使用 unchecked 时要谨慎。跳过溢出检查意味着如果数字确实溢出,你的合约可能会接受无效操作,从而可能导致意外行为或安全风险。因此,只有在你确信不会发生溢出,并且你对此风险感到满意的情况下,才使用 unchecked。
Solidity 是我使用过的第一种语言,其中 mapping 实际上比 array 更便宜!这归结于 EVM 处理数据存储的方式——array 并非顺序存储在内存中,而是更像 mapping 一样工作。虽然你可以通过打包更小的数据类型(如 uint8)来优化 array,但 mapping 不提供这种优势。
话虽如此,mapping 缺乏内置的长度属性,并且不能直接迭代,因此在某些情况下,即使代价更高,你也可能不得不使用 array。array 和 mapping 之间的选择实际上取决于你的具体用例。尽可能使用 mapping 进行查找。它们比 array 在查找和更新值方面更便宜。
Array 适用于有序数据,但在修改元素时可能代价高昂。
结构体打包是 Solidity 中的一种优化技术,通过高效利用存储槽来帮助降低 gas 成本。EVM 以32字节(256位)的槽来存储数据,当创建结构体时,其变量将存储在这些槽中。
结构体打包的工作原理
结构体中的每个变量都占用存储空间,结构体打包的目标是确保多个较小的变量可以容纳在一个32字节的槽中。如果变量排列效率低下,它们可能会溢出到多个槽中,导致更高的 gas 成本。
糟糕打包的示例(更昂贵)
struct User {
uint256 balance; // 占用 32 字节
uint128 rewards; // 开始一个新的槽 (16 字节)
uint256 level; // 占用另一个完整的槽 (32 字节)
}
在这里,level 开始了一个新的槽,尽管前一个槽中有未使用的空间。
优化结构体打包的示例
struct User {
uint128 rewards; // 占用 16 字节
uint128 level; // 与 `rewards` 适合在同一个槽中
uint256 balance; // 开始一个新的槽
}
通过将 rewards 和 level 排列在 balance 之前,我们更好地利用了存储空间,减少了所需的槽数量。
像 && 和 || 这样的逻辑运算符一旦结果已知就会停止评估。利用这一点可以节省 gas。
if (x > 0 && y > 0) {
// 如果 x 为 false,y 永远不会被检查
}
在 Solidity 中,当你回滚交易时,可以提供一条错误消息(通常是字符串),以帮助开发者了解交易失败的原因。然而,在错误消息中使用字符串在 gas 方面可能非常昂贵。
原因在于字符串在 Solidity 中是动态大小的,这意味着它们可能会消耗大量的存储和内存,尤其是在字符串较长时。每次使用字符串时,它都必须被存储然后检索,这会增加 gas 成本。
现在,自定义错误是一种更节省 gas 的替代方案。你不需要使用字符串,而是定义一个带有特定参数的自定义错误,当交易回滚时会传递这些参数。这些错误的编码效率更高,意味着所需的存储和操作更少,最终节省了 gas。
例如:
// 自定义错误定义
error InsufficientBalance(address user, uint256 requested, uint256 available);
// 使用自定义错误回滚
if (balance[msg.sender] < amount) {
revert InsufficientBalance(msg.sender, amount, balance[msg.sender]);
}
在这种情况下,你使用自定义错误而不是像“Insufficient balance”这样的字符串消息,它更有效地打包了数据。这降低了回滚交易的计算成本,并使你的合约在性能和成本方面都更加优化。所以,简而言之,使用自定义错误有助于降低 Solidity 中错误处理相关的 gas 成本,使你的智能合约在性能和成本上都得到优化。
Solidity 中的内联汇编允许开发者直接在智能合约中编写低级代码。它本质上是直接与 EVM 对话,而不是使用高级的 Solidity 语言。
谈到gas 优化,内联汇编可以成为一个强大的工具。这是因为它让你对操作的执行方式拥有更精细的控制,通常比 Solidity 的高级抽象更节省 gas。
原因如下:
示例:
function addNumbers(uint256 a, uint256 b) public pure returns (uint256 result) {
assembly {
result := add(a, b)
}
}
在这个例子中,加法操作是用内联汇编编写的,这比 Solidity 中用于加法的高级函数调用更高效。这个简单的例子可能没有显示出很大的差异,但在更复杂的函数中,汇编可以显著降低 gas 成本。
然而,需要注意一点:内联汇编功能强大但很难正确使用。很容易出错,而且因为它绕过了 Solidity 的一些内置安全检查,所以可能会引入漏洞。建议谨慎使用汇编,并且仅在你确信优化足以证明所增加的复杂性是合理的情况下才使用。
位运算是 gas 优化的另一个强大工具。这些操作允许你直接操作数据的单个位(构成内存中值的0和1)。通过使用位运算,你可以更有效地执行某些任务,在处理某些类型的数据时节省 gas 成本。
为什么位运算能节省 Gas?
示例:使用位 AND、OR 和移位
假设你想将几个标志存储在一个 uint256 变量中。每个标志可以代表合约中的不同条件或状态,你可以将它们打包到一个整数中,而不是为每个标志使用一个单独的布尔变量。
// 使用位运算设置标志的示例
contract FlagStorage {
uint256 flags;
// 设置标志 (位 0)
function setFlag(uint256 flag) public {
flags |= (1 << flag); // 设置对应于标志的位
}
// 检查标志 (位 0)
function checkFlag(uint256 flag) public view returns (bool) {
return (flags & (1 << flag)) != 0; // 检查位是否已设置
}
// 重置标志 (位 0)
function resetFlag(uint256 flag) public {
flags &= ~(1 << flag); // 清除对应于标志的位
}
}
以下是它的工作原理:
setFlag 函数使用位 OR($|$)和左移($<<$)在给定位置(对应于标志)设置位。checkFlag 函数通过对移位后的1执行位 AND($\&$)来检查特定标志是否已设置。如果结果不为零,则表示该标志已设置。resetFlag 函数通过使用位 AND($\&$)和取反的左移($\sim(1 \ll \text{flag})$)来清除特定标志。通过将多个标志打包到一个整数中,你可以减少所需的存储量。你使用的是一个 uint256 来保存多个标志,而不是拥有几个布尔变量(每个都占用一个存储槽)。这不仅更节省 gas,而且在区块链上更节省空间。
在 Solidity 中优化 gas 归结为做出周到的决策。它关乎最大程度地减少存储写入次数,高效组织循环,并为任务选择正确的数据类型。即使是微小的调整,长期来看也能在 gas 成本上带来显著的节省,无论是在部署期间还是用户与你的合约交互时。
随着 Solidity 和以太坊的发展,及时了解新的更新和最佳实践至关重要。这可以确保你的合约保持精简和成本效益,同时不错过可能有助于提高性能的新功能。最终,优化 gas 是为了平衡效率与功能,让你的智能合约能够事半功倍。
- 原文链接: blog.immunebytes.com/gas...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!