精通 Solidity 的 Gas 效率:应对 Base 和其他 L2 链上不断上涨的费用的 12 个技巧

  • cyfrin
  • 发布于 2024-10-22 11:18
  • 阅读 20

本文提供了十二个关于Solidity智能合约的gas优化技巧,旨在帮助开发者在Layer 2链如Base上降低交易费用。技巧包括最小化链上数据、使用映射代替数组、利用常量和不可变量等,旨在有效提高合约的执行效率和降低成本。

介绍了针对 Base 和其他 L2 链的最佳 Solidity gas 优化技术。通过高级、真实世界和经过测试的策略来降低成本。

引言:在 Layer 2 链上优化 Gas 成本

以太坊被广泛认可为领先的 区块链 平台,但关于其波动的交易费用仍然存在持续的担忧。为了解决这个问题,开发了许多 L2 解决方案,如 Base,专注于可扩展性和最小化 gas 成本。尽管 L2 提供了与以太坊主网相比显著降低的 gas 费用,但 智能合约 开发人员仍然有责任在开发过程中优先考虑 gas 优化。这样可以提升用户体验,并创造更具竞争力的去中心化应用(dApps)。

在这篇关于最佳 Solidity gas 优化技巧和技术的指南中,你将学习到由熟练的 web3 开发人员教授的一些高级、真实和经过测试的策略,以减少智能合约的 gas 成本。

请记住,本指南中的示例来自于非常简单的合约,仅用于演示目的。在大多数情况下,它们仅考虑运行时 gas 成本,因为部署成本可能因智能合约的大小而有显著差异。

在实际场景中,我们强烈建议每个智能合约都进行完整深入的 审核过程

有关本文中所有示例和测试,你可以参考 Github gas 优化技巧存储库

这张图片展示了一张包含顶级 Solidity gas 优化技巧以及每个技巧节省的平均 gas 的表格

在开始这篇 web3 开发指南 之前,让我们快速回顾一下 gas 优化的重要性!

‍ Solidity gas 优化的重要性

Gas 优化对开发人员、用户以及项目和协议的长期成功至关重要。有效地优化智能合约的 gas 将使你的协议在成本上更加高效和可扩展,同时降低服务拒绝(DoS)攻击等安全风险。

节省 gas 的合约即使在拥堵的网络条件下也能进行更快、更便宜的交易,从而改善你的产品和用户体验。

简单来说,优化 gas 成本使 Solidity 智能合约、协议和项目:

  • 成本效益高
  • 高效
  • 可用

此外,改善智能合约代码有助于发现潜在的漏洞,使你的协议和用户更加安全。

注意:本指南不能替代 web3 顶级智能合约审计公司 对你合约的彻底安全审查。

总之,在开发过程中,gas 优化应该是一个关键的关注点。它不仅仅是一个“可有可无”的选项,而是确保你的智能合约长期成功和安全的必要条件。仅仅因为你在一个费用相对较低的 L2 上构建,并不意味着你的 gas 费用不会明显高于你的竞争对手!

让我们深入探讨优化 gas 使用的最有效技巧。

免责声明:本指南中的所有 测试均使用 Foundry 执行,设置如下:

  • Solidity 版本:^0.8.13;
  • 本地区块链节点:Anvil
  • 使用的命令:forge test
  • 优化运行次数:100

‍ Solidity gas 优化技巧 ‍

1. 最小化链上数据

NFT 在 2021 年获得了广泛关注,随之而来的是对完全链上 NFT 的日益关注。不同于传统的 NFT,这些 NFT 直接在区块链上存储所有数据,而传统 NFT 则会引用离线数据(如元数据和图像)。这些代币与链上的互动 费用通常很高,而混合解决方案迅速成为常态,因为用户意识到了这对费用的影响。

作为开发者,质疑在链上记录数据的必要性至关重要。无论你是在创建 NFT、游戏还是 DeFi 协议,都应该始终思考哪些数据实际上需要存储在链上,并考虑两种选项的权衡。

通过将信息存储在链下,你可以显著减少智能合约的 gas 消耗,因为你分配的存储空间 fewer 以存储变量。

一种实际的方法是在链上每次调用合约的 vote 函数时,使用事件存储链下数据,而不是直接存储在链上。事件不可避免地增加了交易的 gas 成本(因为需要额外的 emit 函数);然而,通过不将信息存储在链上而节省的开支往往超过了这种成本。

让我们回顾一个允许用户投票 truefalse 的智能合约。在第一个示例中,我们将在链上的一个结构中存储用户的投票。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract InefficientVotingStorage {
    struct Vote {
        address voter;
        bool choice;
    }

    Vote[] public votes;

    function vote(bool _choice) external {
        votes.push(Vote(msg.sender, _choice));
    }
}

测试 vote 函数使用 Foundry 执行超过 100 次后,我们得到以下结果:

测试输出显示 testVotingStorageCost 的成功执行,包含 gas 使用和日志细节。

为了比较,让我们看一个不在链上存储信息的智能合约,而是每次调用 vote 函数时触发一次事件。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

contract EfficientVotingEvents {
    event Voted(address indexed voter, bool choice);

    function vote(bool _choice) external {
        emit Voted(msg.sender, _choice);
    }
}

测试新 vote 函数使用 Foundry 执行超过 100 次后,获得了以下结果:

测试输出显示 testVotingEventsCost 的成功执行,包含 gas 使用、日志和套件结果。

从中可以看出,最小化链上数据平均节省了 90.34% 的 gas。

测试输出显示 testEfficiencyComparisonOnVsOffChain 的成功执行,包含 gas 使用细节,包括使用存储和事件的平均 gas,链外数据存储节省了 90% 的 gas。套件结果显示 1 个测试通过,0 个失败,0 个跳过。

要访问链下数据,可以使用 Chainlink 函数 这样的解决方案,它支持大多数流行的 L2 网络,包括 Base。

使用 Solidity 最小化链上数据的 gas 使用测试结果:

优化前:23,564

优化后:2,274

平均减少:90%

测试链接在 GitHub


‍ 2. 使用映射而非数组

Solidity 提供了两种主要数据结构来管理数据:数组和映射。数组存储一组项目,每个项目分配给特定索引。而映射则是 键值数据结构,通过唯一键提供对数据的直接访问。

虽然数组可能适合存储向量和类似数据,但映射通常因其 gas 效率更受青睐。特别是当需要按需检索数据时,例如名称、钱包地址或账户余额。

为了更好地理解使用数组或映射时产生的 gas 成本,我们需要审查相关的 EVM 操作码操作码 是以太坊虚拟机(EVM)执行智能合约时使用的低级指令,每个操作码都有一个 gas 成本。

通过循环遍历数组中的每个项目以检索值时,我们必须为与之相关的 EVM 操作码消耗的每个 gas 单位付费。

通过以下示例来说明这一概念,我们将展示数组及其在 Solidity 中等效的映射:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract UsingArray {
    struct User {
        address userAddress;
        uint256 balance;
    }

    User[] public users;

    function addUser(address _user, uint256 _balance) public {
        users.push(User(_user, _balance));
    }

    // 仿真从数组提取用户所需的函数
    function getBalance(address _user) public view returns (uint256) {
        for (uint256 i = 0; i < users.length; i++) {
            if (users[i].userAddress == _user) {
                return users[i].balance;
            }
        }
    }
}

在上面的示例中,我们使用数组来存储用户的地址和相应的余额。要获取用户的余额,我们必须遍历每一项,判断 userAddress 是否与 _userAddress 参数匹配,并在匹配的情况下返回余额。对吧?

相反,我们可以使用映射直接访问特定用户的余额,而不必遍历数组中的所有元素:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract UsingMapping {
    mapping(address => uint256) public userBalances;

    function addUser(address _user, uint256 _balance) public {
        userBalances[_user] = _balance;
    }

    // 从映射直接获取用户余额的函数
    function getBalance(address _user) public view returns (uint256) {
        return userBalances[_user];
    }
}

通过在这个测试中用映射替代数组,检索数据使用的 gas 节省了 89%!

测试结果用于 testEfficiencyComparison 显示数组和映射的平均 gas 使用,映射使用 89% 的 gas 节省。

此外,将数据添加到映射的成本比添加到数组低 93%。

测试结果用于 testMappingVsArray 显示添加用户时数组和映射的平均 gas 使用,映射使用 93% 的 gas 节省。

在 Solidity 中检索数据时使用映射而非数组的 gas 使用测试结果:

优化前:30,586

优化后:3,081

平均节省:89%

测试链接在 GitHub


‍ 3. 使用常量和不可变变量以减少智能合约的 gas 成本

优化 Solidity 智能合约的 gas 成本的另一个技巧是使用 常量不可变变量。通过在 Solidity 中将变量声明为不可变或常量,值只有在合约创建期间分配,之后变为只读。

与其他变量不同,这些变量不会消耗 EVM 内部的存储空间。它们的值会直接编译到智能合约字节码中,从而减少与存储操作相关的 gas 成本。

考虑以下示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract InefficientRegularVariables {
    uint256 public maxSupply;
    address public owner;

    constructor(uint256 _maxSupply, address _owner) {
        maxSupply = _maxSupply;
        owner = _owner;
    }
}

如我们在 Foundry 测试中看到的,我们声明了 maxSupply 和 owner 变量,而未使用常量或不可变关键字。运行我们的测试 100 次后,得到的平均 gas 成本为 112,222 个单位。

测试结果用于 testRegularVariablesCost 显示 Anvil 区块链上常规变量的 gas 使用。

由于 maxSupply 和 owner 是已知值且不打算更改,因此我们可以为我们的智能合约声明最大供应量和所有者,而不消耗任何存储空间。

让我们添加常量和不可变关键字,稍微改动声明:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ConstantImmutable {
    uint256 public constant MAX_SUPPLY = 1000;
    address public immutable owner;

    constructor(address _owner) {
        owner = _owner;
    }
}

通过仅仅在 Solidity 智能合约中的变量上添加不可变和常量关键字,我们显著优化了平均 gas 使用,降低了 35.89%。

测试结果用于 testConstantImmutableCost 显示 Anvil 区块链上常量和不可变变量的 gas 使用。

使用常量或不可变变量的 gas 使用测试:

优化前:112,222

优化后:71,940

平均节省:35.89%

测试链接在 Github


‍ 4. 优化未使用的变量

优化 Solidity 智能合约中的变量显而易见地是 gas 优化的一个建议。然而,未使用的变量常常在智能合约执行中被保留,导致不必要的 gas 成本。

让我们看看以下变量使用不当的示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract InefficientContract {
    uint public result;
    uint private unusedVariable = 10;

    function calculate(uint a, uint b) public {
        result = a + b; // 简单操作,作为测试使用
        // 接下来这一行不必要地修改了状态,浪费了 gas。
        unusedVariable = unusedVariable + a;
    }
}

在这个合约中,unusedVariable 被声明,并在 calculate 函数中操作,但它并没有在其他地方被使用。

让我们看看那个未使用变量给我们带来了多少 gas 成本:

测试结果用于 testInefficientContractGas 在 Anvil 区块链上的 gas 使用,显示一个效率低下的合约。

现在,让我们通过移除 unusedVariable 来优化我们的合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract EfficientContract {
    uint public result;

    function calculate(uint a, uint b) public {
        result = a + b; // 仅执行必要的操作
    }
}

测试结果于 testEfficientContractGas 在 Anvil 区块链上,显示高效合约的 gas 使用。

如你所见,仅通过移除一个未使用的变量,我们就能够平均减少 18% 的 gas 成本。

使用测试移除未使用变量的 gas:

优化前:32,513

优化后:27,429

平均节省:18%

测试链接在 Github


‍ 5. Solidity 的 gas 退款,删除未使用的变量

删除未使用的变量并不是简单的“删除”——这将导致内存中的指针出现各种问题。更像是在计算完成后将变量的默认值重新赋值,以避免数据被推送到存储中。

例如,uint 变量的默认值是 0。

让我们看一个简单的例子:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract WithoutDelete {
    uint public data;

    function skipDelete() public {
        data = 123; // 示例操作
        // 在此我们未使用 delete
    }
}

在此情况下,由于在函数结束时未删除数据变量,我们平均支付了 100,300 个 gas 单位,仅用于将该变量赋值给 data

现在让我们看看当我们使用 delete 关键字时会发生什么:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract WithDelete {
    uint public data;

    function useDelete() public {
        data = 123; // 示例操作
        delete data; // 将数据重置为其默认值
    }
}

通过删除我们的 data 变量(将其值重置为 0),我们平均节省了 19% 的 gas!

测试结果用于 testGasUsageComparison,显示使用和不使用删除的 gas 使用,强调节省 19% 的 gas。

删除未使用变量的 gas 使用测试:

优化前:100,300

优化后:80,406

平均节省:19%

测试链接在 Github


‍ 6. 使用固定大小数组而非动态数组来减少智能合约 gas 成本

如前所述,你在可能的情况下应优先使用映射,以优化你的 Solidity 智能合约的 gas。

然而,如果你需要在合约中使用数组,固定大小的数组比动态数组更具 gas 效率,因为动态数组可以无限增长,从而导致更高的 gas 成本。

简单来说,固定大小数组具有已知长度。

表格展示两个元素,n1 和 n2,每个 32 字节,值为 0x01 和 0x02。

相比之下,动态数组可以增长,EVM 必须跟踪其长度并在每次添加新项目时更新它。

表格展示数组长度和两个元素,n1 和 n2,每个 32 字节,值为 0x02、0x01 和 0x02。

让我们来看以下代码,声明一个动态数组,并通过 updateArray 函数更新它:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract DynamicArray {
    uint256[] public dynamicArray;

    constructor() {
        dynamicArray = [1, 2, 3, 4, 5]; // 初始化动态数组
    }

    function updateArray(uint256 index, uint256 value) public {
        require(index < dynamicArray.length, "Index out of range");
        dynamicArray[index] = value;
    }
}

注意,我们使用了 require 语句来确保提供的索引在固定大小数组的范围内。

测试结果用于 testDynamicArrayGas,显示动态数组更新时的 gas 使用,记录在 Anvil 区块链上。

运行我们的测试 100 次的平均支出为 12,541 gas 单位。

现在,让我们将我们的数组修改为大小固定为 5:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract FixedArrayContract {
    uint256[5] public fixedArray;

    constructor() {
        fixedArray = [1, 2, 3, 4, 5]; // 初始化固定大小数组
    }

    function updateArray(uint256 index, uint256 value) public {
        require(index < fixedArray.length, "Index out of range");
        fixedArray[index] = value;
    }
}

在这个示例中,我们定义了一个长度为 5 的 uint256 型固定大小数组。updateArray 函数与之前相同,允许我们更新数组中特定索引的值。

现在 EVM 知道状态变量 fixedArray 的大小为 5,并将为其分配 5 个插槽,而不需要在存储中存储其长度。

运行相同的 Foundry 测试 100 次,使用固定数组代替动态数组,节省了 17.99% 的 gas。

测试结果用于 testFixedSizeArrayGas,显示固定大小数组更新的 gas 使用,记录在 Anvil 区块链上。

在 Solidity 中使用固定大小数组的 gas 使用测试:

优化前:12,541

优化后:10,284

平均节省:17.99%

测试链接在 Github


‍ 7. 避免使用小于 256 位的变量

在 Solidity 中,使用 uint8 而非 uint256在某些上下文中可能会低效且潜在增加成本,主要是由于 EVM 的运行方式。

EVM 的字长为 256 位。因此,对 256 位整数(uint256)的操作通常是最高效的,因为其与 EVM 的原生字长对齐。

当你使用较小的整数(如 uint8)时,Solidity 通常需要执行额外的操作,以将这些较小的类型对齐到 EVM 的 256 位字长。这导致代码更复杂,效率低下。

虽然使用较小类型(如 uint8)在优化存储方面可能是有利的(因为多个 uint8 变量可以被打包到一个 256 位的存储插槽中),但这种好处通常仅在存储中体现,而不是在内存或栈操作中。

此外,进行 uint256 的转换时,数据的存储节省可能抵消。

uint8 public a = 12;
uint256 public b = 13;
uint8 public c = 14;

// 这可能导致效率低下及增加 gas 成本
// 由于 EVM 对 256 位操作的优化。

总而言之,尽管使用 uint8 看似可以节省空间并降低成本,但实际上可能导致效率低下,增加 gas 成本,因为 EVM 优化 256 位操作。

uint256 public a = 12;
uint256 public b = 14;
uint256 public c = 13;

// 更好的解决方案

你可以创建一个传递 raw 字节参数的事务 f(uint8 x),其中有 0xff0000010x00000001。这两者都提供给合约,并将被认为是数字 1。然而,msg.data 在每种情况下会有所不同。因此,如果你的代码实现了像 keccak256(msg.data) 的逻辑,你将得到不同的结果。


‍ 8. 将小于 256 位的变量打包在一起

如前所述,使用小于 256 位的 intuint 变量通常被认为不如使用 256 位的变量高效。然而,在某些情况下,你可能被迫使用较小的类型,例如在使用权重为 1 字节或 8 位的布尔型时。

在这些情况下,通过声明具有存储空间考虑的状态变量,Solidity 将允许你打包它们并将它们全部存储在同一个插槽中。

注意:打包变量的好处通常仅在存储中体现,而不是在内存或栈操作中。

考虑以下示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract NonPackedVariables {
    bool public a = true;
    uint256 public b = 14;
    bool public c = false;

    function setVariables(bool _a, uint256 _b, bool _c) public {
        a = _a;
        b = _b;
        c = _c;
    }
}

考虑到我们之前提到的在 Solidity 中每个存储插槽的空间为 32 字节(等同于 256 位),在上述示例中,我们需要使用三个存储插槽来存储我们的变量:

  • 1 个存储我们的布尔变量 “ a” (1 字节)
  • 1 个存储我们的 uint256 变量 “ b” (32 字节)
  • 1 个存储我们的布尔变量 “ c” (1 字节)。

每个使用的存储插槽都会产生 gas 成本,因此我们花费了三倍的成本。

鉴于两个布尔变量的总大小为 16 位,这比单个存储插槽的容量少 240 位,我们可以指导 Solidity 将变量 " a" 和 " c" 存放在同一个插槽内,也就是 “我们可以将它们打包一起。”

打包变量允许你通过减少所需的插槽数量,降低部署 gas 成本。

我们可以通过调整它们的声明顺序来打包这些变量,如下所示:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract PackedVariables {
    bool public a = true;
    bool public c = false;
    uint256 public b = 14;

    function setVariables(bool _a, bool _c, uint256 _b) public {
        a = _a;
        c = _c;
        b = _b;
    }
}

重新排列这些变量允许 Solidity 将两个布尔变量包放在同一个插槽中,因为它们的权重大于 256 位(32 字节)。

然而,我们仍可能会浪费存储空间。EVM 在 256 位的字上操作,并会执行操作来标准化更小的字,这可能抵消潜在的 gas 节省。

运行我们在 Foundry 中经过 100 次迭代的测试,我们获得平均 13% gas 的优化。

测试结果用于 testEfficiencyComparisonPacked,显示打包和未打包变量的 gas 使用,打包变量优势为 13%。

在 Solidity 中打包变量的 gas 使用测试:

优化前:1,678

优化后:1,447

平均节省:13%

测试链接在 Github


‍ 9. 使用外部可见性修饰符

在 Solidity 中,选择最合适的函数可见性是一种有效的优化智能合约 gas 消耗的方式。具体来说,使用 external 可见性修饰符比 public 更加 gas 高效。

原因在于 public 函数如何处理参数,以及数据如何传递到这些函数。

External 函数可以从 calldata 中读取,这里是 EVM 中存储函数调用参数的只读暂存区。使用 calldata 是外部调用更加节省 gas 的原因,避免将数据从交易数据复制到内存中。

另一方面,public 函数可以在链内部(来自合约)和外部进行调用。當外部调用时,它们的行为类似于外部函数,参数在交易数据中传递。然而,当在内部调用时,参数以 memory 的形式进行传递,而不是通过 calldata

简单来说,由于 public 函数需要支持内部和外部调用,因此它们无法仅访问 calldata。

考虑以下 Solidity 合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract PublicSum {
    function calculateSum(
        uint[] memory numbers
    ) public pure returns (uint sum) {
        for (uint i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
    }
}

该函数计算数字数组的总和。由于该函数是 public,因此它必须接受来自内存的大数组,这在 gas 方面可能会成本不菲。

测试结果用于 testPublicSumGas,显示使用 Anvil 区块链 200 元素数组的公共函数的 gas 使用。

现在让我们通过将这个函数改为 external 来修改它:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ExternalSum {
    function calculateSum(
        uint[] calldata numbers
    ) external pure returns (uint sum) {
        for (uint i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
    }
}

将函数改为 external 后,我们现在可以从 calldata 接受数组,在处理大数组时变得更高效。

这突出了在你的 Solidity 智能合约中正确使用可见性修饰符以优化 gas 使用的重要性。

在这种情况下,修改你的 Solidity 函数修饰符将每次调用节省平均 0.3% 的 gas 单位。

在 Solidity 中使用 external 修饰符的 gas 使用测试:

优化前:495,234

优化后:493,693

平均节省:0.3%

测试链接在 Github。


‍ 10. 缓存存储以避免重复读取同一值

当你确认多种方法实现相同功能时,审查相关 EVM 操作码消耗的 gas 是值得的,找到最Gas费高效的方案。

节省 gas 的一个有效方法是避免重复从存储中读取相同的变量。读取存储的费用高于读取内存。如果你多次使用一个值,将其从存储中的读取一次然后缓存到内存中,然后在其他实例中从内存中读取是更省 gas 的方法。

很多智能合约的 gas 浪费源于以下两个问题:

1. 重复读取同一值 2. 不必要地写入存储。

在下面的合约中,numbers 数组存储在存储中。每次循环 iterating sumNumbers 函数的时候,numbers 变量都从存储中读取。

contract ReadStorage {
   uint256[] public numbers;

   function addNumbers(uint256[] memory newNumbers) public {
       for (uint256 i = 0; i < newNumbers.length; i++) {
           numbers.push(newNumbers[i]);
       }
   }

   function sumNumbers() public view returns (uint256) {
       uint256 sum = 0;
       for (uint256 i = 0; i < numbers.length; i++) {
           sum += numbers[i];
       }
       return sum;
   }
}

为了避免这个,我们可以在函数开始时创建一个存储在内存中的变量,并将其值赋给 numbers 变量。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CacheStorage {
   uint256[] public numbers;

   function addNumbers(uint256[] memory newNumbers) public {
       for (uint256 i = 0; i < newNumbers.length; i++) {
           numbers.push(newNumbers[i]);
       }
   }

   function sumNumbers() public view returns (uint256) {
       uint256 sum = 0;
       uint256[] memory numbersArray = numbers;
       for (uint256 i = 0; i < numbersArray.length; i++) {
           sum += numbersArray[i];
       }
       return sum;
   }
}

这将 sumNumbers 函数的 gas 消耗减少了 17%,但随着数组的增长,迭代次数和 gas 成本也会增加。

测试结果用于 testEfficiencyComparisonCaching,显示使用和不使用缓存的 gas 使用,缓存带来 17% 的 gas savings。

在 Solidity 中缓存变量的 gas 使用测试:

优化前:3,527

优化后:2,905

平均节省:17%

测试链接在 Github


‍ 11. 避免将变量初始化为默认值

当你确定多种实现相同功能的方法时,审查相关 EVM 操作码消耗的 gas 是值得的,以找到最 gas 高效的方案。

当我们声明状态变量而未初始化时(未分配初始值),它们在合约部署时会自动初始化为默认值。默认值为:

  • 整数为 0
  • 布尔为 false
  • 地址为 address(0)

这比将它们的值声明为默认值更省 gas。

让我们比较不同声明中产生相同结果的情况。

uint256 number; //这成本较低
uint256 number = 0; //这成本更高

bool claim; //这成本较低
bool claim = false; //这成本更高

address owner;  //这成本较低
address owner = address(0); //这成本更高

通常会发现状态变量被分配为默认值,并在用户交互后立即更改,这并不是 gas 效率很高的方式。

让我们看看两个简单合约的状态变量类型 uint256。在第二个示例中,我们未初始化变量,因此将在部署时分配其默认值。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract WithoutInitializing {
   uint256 counter = 0;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract WithInitializing {
   uint256 counter;
}

通过不在我们的 Solidity 智能合约中初始化变量,我们平均优化了花费的 gas 达到 4%。测试结果显示初始化变量的 gas 使用情况,未初始化时节省了 4% 的 gas。

使用 Solidity 中变量默认值的 Gas 使用测试:

优化前:46,996

优化后:44,802

平均节省:4%

测试链接 Github

‍ 12. 启用 Solidity 编译器优化

Solidity 配备了一个可以轻松修改设置以优化你编译代码的编译器。

Solidity 编译器 看作是法师的咒语书,而你对它选项的智能操作可以创造出显著减少 gas 使用的优化药水。

--optimize 选项就是你可以施展的一个咒语。

当启用时,它会进行数百次运行,简化你的字节码并将其转换为使用更少 gas 的精简版本。

然后可以调整编译器,以在部署和运行时成本之间取得适当的平衡。

例如,使用 --runs 命令可以让你定义合约的预计执行次数。

  • 较高的数字:编译器优化合约执行时的 gas 成本。
  • 较低的数字:编译器优化合约部署时的 gas 成本。

solc --optimize --runs=200 GasOptimized.sol

通过使用 --optimize 标志并指定 --runs=200,我们指示编译器优化代码以减少合约执行期间的 gas 消耗,通过运行 incrementCount 函数 200 次。

根据你应用的独特需求调整这些设置。

‍ 13. 备用 Solidity Gas 优化提示:使用 Assembly*

当你编译 Solidity 智能合约时,编译器将其转换为字节码,即一系列 EVM 操作码。

通过使用 assembly,你可以编写与操作码紧密对齐的代码。

虽然在如此低的级别编写代码可能不是一项容易的任务,但其优势在于能够手动优化操作码,从而在某些情况下超越 Solidity 字节码。

这种优化级别使合同执行中的效率和有效性更高。

在一个简单的示例中,有两个函数用于相加两个数字,一个使用普通 Solidity,另一个使用 Assembly,虽然存在小差异,但 Assembly 版本仍然更便宜。

Solidity 示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract InefficientAddSolitiy {
    // 标准 Solidity 函数来添加两个数字
    function addSolidity(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }
}

现在实现 Assembly:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract EfficientAddAssembly {
    // 使用 assembly 添加两个数字的函数
    function addAssembly(
        uint256 a,
        uint256 b
    ) public pure returns (uint256 result) {
        assembly {
            result := add(a, b)
        }
    }
}

我们想特别提及 Huff,它使我们能够以更优美的语法编写 Assembly。

注意:即使使用 Assembly 可能帮助你优化智能合约的 gas 成本,但它也可能导致代码不安全。我们强烈建议你在部署之前让 智能合约安全专家 审核你的合约。

‍ 结论

在 Solidity 中优化 gas 使用对创建具有成本效益、高性能和可持续性的 Solidity 智能合约至关重要。

在 L2(如 Base)上部署项目将减少用户使用你协议的成本,但作为开发者,你仍需负责实施你在本指南中学到的 Solidity gas 优化技巧。这些技巧可以显著降低交易成本,提高可扩展性并增强你合约的整体效率。

  • 原文链接: cyfrin.io/blog/solidity-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
cyfrin
cyfrin
Securing the blockchain and its users. Industry-leading smart contract audits, tools, and education.