Solidity 智能合约中的 Gas 优化:开发者指南 – ImmuneBytes

本文深入探讨了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 中的 Gas 成本

1. 存储(Storage) vs. 内存(Memory) vs. 调用数据(Calldata)

Solidity 提供了三种主要的数据存储方式:

  • 存储(Storage):最昂贵的选项,因为数据永久写入区块链。
  • 内存(Memory):一个临时性的、事务内的空间,执行后会被清除。
  • 调用数据(Calldata):一种经济高效的只读数据空间,主要用于函数参数。

在传递参数时,尽可能使用 calldata 而不是 memory,以降低 gas 成本。

2. 昂贵操作 vs. 廉价操作

并非所有操作都消耗相同数量的 gas。有些操作比其他操作昂贵得多:

  • 昂贵:写入存储(sstore)、部署合约、发出事件。
  • 中等:从存储读取(sload)、调用外部合约。
  • 廉价:简单的数学运算、从内存读取、使用 immutable 变量。

了解哪些操作消耗的 gas 最多有助于你编写更高效的合约。

实用 Gas 优化技巧

1. 优化变量使用

变量打包以节省空间

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位字。

2. 优化存储访问

减少冗余的存储写入

写入存储是 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 退款

3. 函数级 Gas 优化

使用 external 而不是 public

旨在进行外部调用的函数应标记为 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。

4. 循环效率和迭代优化

降低循环中的计算复杂度

循环可能代价高昂,尤其是在处理大型数组时。通过只获取一次数组长度而不是在循环内部多次调用 .length 来避免不必要的迭代。

function processArray(uint256[] calldata data) external {
    uint256 length = data.length;
    for (uint256 i = 0; i < length; i++) {
        // 处理数据
    }
}

使用 unchecked 跳过溢出检查

算术运算,如加法、减法和乘法,通常包含溢出检查,以确保结果保持在数据类型(例如 uint256)的有效值范围内。如果发生溢出,Solidity 会自动回滚交易以防止意外行为。

然而,这些检查会消耗额外的 gas,因为 EVM 需要执行比较以确保不会发生溢出。如果你确信某个操作不会溢出(例如,当你知道所涉及的数字足够小),你可以使用 unchecked 关键字跳过这些溢出检查,这可以节省 gas。

为什么 unchecked 能够节省 Gas?

  1. 没有额外比较:通过使用 unchecked,Solidity 不需要对每个操作执行溢出检查。结果是它跳过了验证操作是否在界限内的计算开销,从而略微降低了这些操作的 gas 消耗。
  2. 优化简单算术:在某些情况下,你的合约可能涉及你知道值不会溢出的操作(例如,两个小数相加或对固定大小数组执行操作)。在这里使用 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

5. 数据结构选择以提高 Gas 效率

Mappings vs. Arrays

Solidity 是我使用过的第一种语言,其中 mapping 实际上比 array 更便宜!这归结于 EVM 处理数据存储的方式——array 并非顺序存储在内存中,而是更像 mapping 一样工作。虽然你可以通过打包更小的数据类型(如 uint8)来优化 array,但 mapping 不提供这种优势。

话虽如此,mapping 缺乏内置的长度属性,并且不能直接迭代,因此在某些情况下,即使代价更高,你也可能不得不使用 array。array 和 mapping 之间的选择实际上取决于你的具体用例。尽可能使用 mapping 进行查找。它们比 array 在查找和更新值方面更便宜。

Array 适用于有序数据,但在修改元素时可能代价高昂。

结构体打包(Struct Packing)

结构体打包是 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;  // 开始一个新的槽
}

通过将 rewardslevel 排列在 balance 之前,我们更好地利用了存储空间,减少了所需的槽数量。

6. 编写高效代码

使用短布尔表达式

&&|| 这样的逻辑运算符一旦结果已知就会停止评估。利用这一点可以节省 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 成本,使你的智能合约在性能和成本上都得到优化。

7. 高级 Gas 优化策略

使用内联汇编(Inline Assembly)

Solidity 中的内联汇编允许开发者直接在智能合约中编写低级代码。它本质上是直接与 EVM 对话,而不是使用高级的 Solidity 语言。

谈到gas 优化,内联汇编可以成为一个强大的工具。这是因为它让你对操作的执行方式拥有更精细的控制,通常比 Solidity 的高级抽象更节省 gas。

原因如下:

  1. 直接 EVM 操作:Solidity 的语法旨在可读性和安全性,这意味着它有时会增加额外的步骤或检查以确保安全。内联汇编绕过了这些高级保护,直接与 EVM 交互,允许你更快、以更少的开销执行某些操作。例如,Solidity 中的算术操作或内存操作可能会因为抽象而使用比必要更多的 gas,但在汇编中,你可以避免不必要的步骤。
  2. 优化的字节码:内联汇编允许你编写更高效的字节码。Solidity 编译器通常会为检查和处理边缘情况等添加额外的代码,但汇编允许你精确地编写所需的内容——不多不少。这可以减少已部署合约的大小,从而在部署和执行期间节省 gas。
  3. 低级内存和存储访问:当与存储或内存交互时,Solidity 在幕后执行某些操作,这些操作并非总是最节省 gas 的。内联汇编允许你直接操作内存和存储,从而在访问或修改数据时可能节省 gas。例如,使用内联汇编直接写入内存可能比使用 Solidity 的高级存储操作更便宜。

示例:

function addNumbers(uint256 a, uint256 b) public pure returns (uint256 result) {
    assembly {
        result := add(a, b)
    }
}

在这个例子中,加法操作是用内联汇编编写的,这比 Solidity 中用于加法的高级函数调用更高效。这个简单的例子可能没有显示出很大的差异,但在更复杂的函数中,汇编可以显著降低 gas 成本。

然而,需要注意一点:内联汇编功能强大但很难正确使用。很容易出错,而且因为它绕过了 Solidity 的一些内置安全检查,所以可能会引入漏洞。建议谨慎使用汇编,并且仅在你确信优化足以证明所增加的复杂性是合理的情况下才使用。

使用位运算(Bitwise Operations)

位运算是 gas 优化的另一个强大工具。这些操作允许你直接操作数据的单个位(构成内存中值的0和1)。通过使用位运算,你可以更有效地执行某些任务,在处理某些类型的数据时节省 gas 成本。

为什么位运算能节省 Gas?

  1. 较低的计算成本:位运算是非常低级的操作,这意味着它们不需要像高级算术或条件检查那样多的计算开销。
  2. 紧凑的数据存储:位运算可以帮助你将多个值打包到一个存储槽或一个变量中。例如,如果你需要存储几个小的信息片段(如布尔标志或小整数),你可以使用位移和掩码将它们打包到一个数字中。这减少了你需要的变量或存储槽的数量,从而节省了存储操作的 gas。
  3. 高效的标志和掩码:当你处理布尔标志或某些类型的状态码时,位运算可以非常有效地组合或提取这些标志。

示例:使用位 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ImmuneBytes
ImmuneBytes
Stay Ahead of the Security Curve.