Solidity优化 - 减少智能合约gas消耗

1. 首选数据类型

尽量使用 256 位的变量,例如 uint256 和 bytes32!乍一看,这似乎有点违反直觉,但是当你更仔细地考虑以太坊虚拟机(EVM)的运行方式时,这完全有意义。每个存储插槽都有 256 位。因此,如果你只存储一个 uint8,则 EVM 将用零填充所有缺少的数字,这会耗费 gas。此外,EVM 执行计算也会转化为 uint256 ,因此除 uint256 之外的任何其他类型也必须进行转换。

注意:通常,应该调整变量的大小,以便填满整个存储插槽。

2. 在合约的字节码中存储值

一种相对便宜的存储和读取信息的方法是,将信息部署在区块链上时,直接将其包含在智能合约的字节码中。不利之处是此值以后不能更改。但是,用于加载和存储数据的 gas 消耗将大大减少。有两种可能的实现方法:

  1. 将变量声明为 constant 常量 (译者注:声明为 immutable同样也可以降低 gas,测试constant比immutable更加节省gas)
  2. 在你要使用的任何地方对其进行硬编码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;

contract CryptosTribeToken  {
   uint256 add;
   uint256 public v1;
   uint256 public immutable v2=10;

    function calculate() public view returns (uint256 result) {
        return v1 * v2 * 10000;
    }
}

3. 通过 SOLC 编译器将变量打包到单个插槽中

当你将数据永久存储在区块链上时,要在后台执行汇编命令 SSTORE。这是最昂贵的命令,费用为 20,000 gas,因此我们应尽量少使用它。在内部结构体中,可以通过简单地重新排列变量来减少执行的 SSTORE 操作量,如以下示例所示:

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

contract CryptosTribeToken  {
   uint256 add;
   uint256 public v1;
   uint256 public immutable v2=10;

    function calculate() public view returns (uint256 result) {
        return v1 * v2 * 10000;
    }
        struct Data {
        uint64 a;
        uint64 b;
        uint128 c;
        uint256 d;
    }
    Data public data;
    constructor(uint64 _a, uint64 _b, uint128 _c, uint256 _d)  {
        data.a = _a;
        data.b = _b;
        data.c = _c;
        data.d = _d;
    }
}

请注意,在 struct 中,所有可以填充为 256 位插槽的变量都彼此相邻排序,以便编译器以后可以将它们堆叠在一起(也使用占用少于 256 位的那些变量)。在上面的例子中,仅使用两次 STORE 操作码,一次用于存储abc,另一次用于存储d这同样适用于在结构体外部的变量。另外,请记住,将多个变量放入同一个插槽所节省的费用要比填满整个插槽(首选数据类型)所节省的费用大得多

如果将结构体Data中c和d位置调换,那么将使用3次store,a和b一次,c一次,d一次,gas就会币之前多

4. 通过汇编将变量打包到单个插槽中

也可以手动应用将变量堆叠在一起以减少执行的 SSTORE 操作的技术。下面的代码将 4 个 uint64 类型的变量堆叠到一个 256 位插槽中。

编码:将变量合并为一个。

注意:请记得使用编译器打包优化

function encode(uint64 _a, uint64 _b, uint64 _c, uint64 _d) internal pure returns (bytes32 x) {
    assembly {
        let y := 0
        mstore(0x20, _d)
        mstore(0x18, _c)
        mstore(0x10, _b)
        mstore(0x8, _a)
        x := mload(0x20)
    }
}

为了读取,将需要对该变量进行解码,这可以通过第二个功能实现。

解码:将变量拆分为其初始部分。

function decode(bytes32 x) internal pure returns (uint64 a, uint64 b, uint64 c, uint64 d) {
    assembly {
        d := x
        mstore(0x18, x)
        a := mload(0)
        mstore(0x10, x)
        b := mload(0)
        mstore(0x8, x)
        c := mload(0)
    }
}

将这种方法的 gas 消耗量与上述方法的 gas 消耗量进行比较,你会注意到,由于多种原因,这种方法的成本明显降低:

  1. **精度:**使用这种方法,就位打包而言,几乎可以做任何事情。例如,如果已经知道不需要变量的最后一位,则可以通过将正在使用的 1 位变量与 256 位变量合并在一起进行优化。
  2. **读取一次:**由于变量实际上存储在一个插槽中,因此只需执行一次加载操作即可接收所有变量。如果变量在一起使用,这将特别有益。

那么,为什么还要使用以前的呢?从这两种实现来看,很明显,我们使用汇编来解码变量,就放弃了代码的可读性,因此,使第二种方法更容易出错。另外,由于每种情况下我们都必须包含编码和解码函数,因此部署成本也将大大增加。但是,如果你确实需要降低函数的 gas 消耗, (与其他方法相比,装入单个插槽中的变量越多,节省的费用就越高。)

猜你喜欢

转载自blog.csdn.net/toume/article/details/125445131