Gas 优化

省钱总结

  • 使用短路模式来排序操作

  • 函数使用external + calldata

    • memory 类型的参数,使用 external 并且参数标注 calldata 是最省钱的。

  • 使用正确的数据类型

    • gas 由小到大: 值类型 < 引用类型

  • 循环中不操作 storage 变量

  • 循环中的不重复计算数据

    • 能不在循环中操作的数据,就尽量放在外面计算

  • 多个循环可以合并就尽量合并.(循环尽量不用)

  • 可预测的结果,不通过代码计算。

  • 避免死代码

  • 避免不必要的判断

  • 删除不必要的库

  • 函数中能不 retrun,就尽量不 return

  • 库合约使用 using for 比直接使用更省 gas

  • mapping 的 key 使用 bytes32,而不使用字符串,可以省 Gas;

  • private 变量比 public变量更节省 Gas

使用短路模式来排序操作

短路(short-circuiting)是利用逻辑或(||),逻辑与(&&)的特性,来排序不同成本操作的开发模式;核心是 它将低 gas 成本的操作放在前面,高 gas 成本的操作放在后面;这样如果前面的低成本操作不可行,就可以跳过(短路)后面的高成本以太坊操作了。

  • 前面判断为 true,执行后面的条件判断

  • 前面判断为 false,跳过后面的条件判断

    • 这样就把后面条件的运行 gas 给节省下来了

// f(x) 是低gas成本的操作
// g(y) 是高gas成本的操作

// 按如下排序不同gas成本的操作
f(x) || g(y)
f(x) && g(y)

这里哪个判断在前面,哪个判断在后面,需要根据实际情况来安排。

下面是例子说明,一个非常简单的值判断:

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

contract Demo {
    // gas值如下
    // 输入 1:   22050
    // 输入 2:   22065
    // 输入 101: 22050
    // 输入 102: 22055
    // 平均值: (22050+22065+22050+22055)/4 = 22055
    function test1(uint256 _amount) external pure returns (bool) {
        bool isEven = _amount % 2 == 0;
        bool isLessThan99 = _amount < 99;
        if (isEven && isLessThan99) {
            return true;
        }
        return false;
    }

    // gas值如下
    // 输入 1:   22040
    // 输入 2:   22061
    // 输入 101: 22040
    // 输入 102: 22051
    // 平均值: (22040+22061+22040+22051)/4 = 22048
    function test2(uint256 _amount) external pure returns (bool) {
        if (_amount % 2 == 0 && _amount < 99) {
            return true;
        }
        return false;
    }

    // gas值如下
    // 输入 1:   22073
    // 输入 2:   22083
    // 输入 101: 21881
    // 输入 102: 21881
    // 平均值: (22073+22083+21881+21881)/4 = 21979.5 ✅ 平均值最低
    function test3(uint256 _amount) external pure returns (bool) {
        if (_amount < 99 && _amount % 2 == 0) {
            return true;
        }
        return false;
    }
}

函数使用external + calldata

在合约开发种,显式声明函数的可见性不仅可以提高智能合约的安全性,同时也有利于优化合约执行的 gas 成本。

例如,通过显式地标记函数为外部函数 external,可以强制将函数参数的存储位置设置为 calldata,这会节约每次函数执行时所需的以太坊 gas 成本。

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

contract Demo {
    // gas值如下
    // 输入 a       :   22965
    // 输入 abc     :   22989
    // 输入 hello   :   23013
    // 平均值: (22965+22989+23013)/3 = 22989
    function test1(string memory _str) public pure returns (string memory) {
        return _str;
    }

    // gas值如下
    // 输入 a       :   22943
    // 输入 abc     :   22967
    // 输入 hello   :   22991
    // 平均值: (22943+22967+22991)/3 = 22967
    function test2(string memory _str) external pure returns (string memory) {
        return _str;
    }

    // gas值如下
    // 输入 a       :   22705
    // 输入 abc     :   22729
    // 输入 hello   :   22753
    // 平均值: (22705+22729+22753)/3 = 22729 ✅ 平均值最低
    function test3(string calldata _str) external pure returns (string memory) {
        return _str;
    }
}

注意这种方法只对 memory 类型的数据有效,如果是操作普通的类型,可见性没有影响。如下例子我将上面短路操作例子中 external 改为 public,所消耗的 gas 并没有任何影响

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

contract Demo {
    // gas值如下
    // 输入 1:   22050  | 改为 public 后: 22050
    // 输入 2:   22065  | 改为 public 后: 22065
    // 输入 101: 22050  | 改为 public 后: 22050
    // 输入 102: 22055  | 改为 public 后: 22055
    // 平均值: (22050+22065+22050+22055)/4 = 22055
    function test1(uint256 _amount) public pure returns (bool) {
        bool isEven = _amount % 2 == 0;
        bool isLessThan99 = _amount < 99;
        if (isEven && isLessThan99) {
            return true;
        }
        return false;
    }

    // gas值如下
    // 输入 1:   22040  | 改为 public 后: 22040
    // 输入 2:   22061  | 改为 public 后: 22061
    // 输入 101: 22040  | 改为 public 后: 22040
    // 输入 102: 22051  | 改为 public 后: 22051
    // 平均值: (22040+22061+22040+22051)/4 = 22048
    function test2(uint256 _amount) public pure returns (bool) {
        if (_amount % 2 == 0 && _amount < 99) {
            return true;
        }
        return false;
    }

    // gas值如下
    // 输入 1:   22073  | 改为 public 后: 22073
    // 输入 2:   22083  | 改为 public 后: 22083
    // 输入 101: 21881  | 改为 public 后: 21881
    // 输入 102: 21881  | 改为 public 后: 21881
    // 平均值: (22073+22083+21881+21881)/4 = 21979.5 ✅ 平均值最低
    function test3(uint256 _amount) public pure returns (bool) {
        if (_amount < 99 && _amount % 2 == 0) {
            return true;
        }
        return false;
    }
}

使用正确的数据类型

有些数据类型要比另外一些数据类型的 gas 成本高。我们有必要了解可用数据类型的 gas 利用情况,以便根据你的需求选择效率最高的那种。下面是关于数据类型 gas 消耗情况的一些规则:

  • 在任何可以使用 uint256 类型的情况下,不要使用 string 类型

  • 存储 uint256 要比存储 uint8 的 gas 成本低,为什么?点击这里 查看原文

  • 当可以使用 bytes32 类型时,不要使用 byte[] 类型

  • 如果 bytes32 的长度有可以预计的上限,那么尽可能改用 bytes1~bytes32 这些具有固定长度的类型

  • bytes32 所需的 gas 成本要低于 string 类型

  • bool > unit256 > uint8 > bytes1 > bytes32 > bytes > string

存储对比

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

contract Demo {
    uint256 public test3 = 0; // 23494
    uint8 public test1 = 0; // 23554
    bool public test2 = false; // 23630
    bytes1 public test4 = 0x30; // 23601
    // 23516
    bytes32 public test5 =
        0x3000000000000000000000000000000000000000000000000000000000000000;
    string public test7 = "0"; // 24487
    bytes public test6 = bytes("0"); // 24531
}

返回对比

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

contract Demo {
    // uint256 :21415
    function test1() external pure returns (uint256) {
        return 0;
    }

    // uint8 : 21444
    function test2() external pure returns (uint8) {
        return 0;
    }

    // bool :21400
    function test3() external pure returns (bool) {
        return false;
    }

    // bytes1 :21479
    function test4() external pure returns (bytes1) {
        return 0x30;
    }

    // bytes32 :21430
    function test5() external pure returns (bytes32) {
        return
            0x3000000000000000000000000000000000000000000000000000000000000000;
    }

    // bytes :21845
    function test6() external pure returns (bytes memory) {
        return bytes("0");
    }

    // string :21801
    function test7() external pure returns (string memory) {
        return "0";
    }
}

循环中不操作 storage 变量

管理 storage 变量的 gas 成本要远远高于内存变量,所以要避免在循环中操作 storage 变量。

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

contract Demo {
    uint256 public num = 0;

    // 输入 5 : 46475 gas
    function test1(uint256 x) public {
        for (uint256 i = 0; i < x; i++) {
            num += 1;
        }
    }

    // 输入 5 : 45651 gas
    function test2(uint256 x) public {
        uint256 temp = num;
        for (uint256 i = 0; i < x; i++) {
            temp += 1;
        }
        num = temp;
    }
}

循环中的不重复计算数据

  • 能不在循环中操作的数据,就尽量放在外面计算

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

contract Demo {
    uint256 a = 4;
    uint256 b = 5;

    function repeatedComputations(uint256 x) public view returns (uint256) {
        uint256 sum = 0;
        for (uint256 i = 0; i <= x; i++) {
            sum = sum + a * b;
        }
    }
}

尽量少用循环

多个循环可以合并就尽量合并,循环尽量不用

有时候在 Solidity 智能合约中,你会发现两个循环的判断条件一致,那么在这种情况下就没有理由不合并它们。例如下面的以太坊合约代码:

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

contract Demo {
    function loopFusion(uint256 x, uint256 y) public pure returns (uint256) {
        for (uint256 i = 0; i < 100; i++) {
            x += 1;
        }
        for (uint256 i = 0; i < 100; i++) {
            y += 1;
        }
        return x + y;
    }
}

可预测的结果,不通过代码计算。

如果一个循环计算的结果是无需编译执行代码就可以预测的,那么就不要使用循环,这可以可观地节省 gas。

例如下面的以太坊合约代码就可以直接设置 num 变量的值:

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

contract Demo {
    function constantOutcome() public pure returns (uint256) {
        uint256 num = 0;
        for (uint256 i = 0; i < 100; i++) {
            num += 1;
        }
        return num;
    }
}

避免死代码

死代码(Dead code)是指那些永远也不会执行的 Solidity 代码,例如那些执行条件永远也不可能满足的代码,就像下面的两个自相矛盾的条件判断里的代码块,消耗了以太坊 gas 资源但没有任何作用:

if (x < 1) {
    if (x > 2) {
        return x;
    }
}

避免不必要的判断

有些条件断言的结果不需要代码的执行就可以了解,那么这样的条件判断就可以精简掉。例如下面的 Solidity 合约代码中的两级判断条件,最外层的判断是在浪费宝贵的以太坊 gas 资源:

 if(x < 1) {
    if(x < 0) {
      return x;
    }
  }

删除不必要的库

在开发 Solidity 智能合约时,我们引入的库通常只需要用到其中的部分功能,这意味着其中可能会包含大量对于我们的智能合约而言是冗余代码。如果可以在自己的合约里安全有效地实现所依赖的库功能,那么就能够达到优化合约的 gas 利用的目的。

例如,在下面的 solidity 代码中,我们的以太坊合约只是用到了 SafeMath 库的 add 方法:

import './SafeMath.sol' as SafeMath;

contract SafeAddition {
  function safeAdd(uint a, uint b) public pure returns(uint) {
    return SafeMath.add(a, b);
  }
}
contract SafeAddition {
  function safeAdd(uint a, uint b) public pure returns(uint) {
    uint c = a + b;
    require(c >= a, "Addition overflow");
    return c;
  }
}

优化案例

本篇主要介绍 gas 的优化。

  • 函数输入参数:使用 calldata ,不使用 memory

  • 读取状态变量:使用。

  • 使用数组时候:

    • for 循环时,缓存数组长度

    • 储存数组的元素到 memory

  • short circuit: && 短路操作

    • A && B,A 表达式 不成立,则不计算 B 表达式

  • loop increments

下面默认的 Gas 是 50518 gas

原始代码:

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

contract Gas {
    uint256 public total;

    // [1,2,3,4,5,100]
    function demo(uint256[] memory nums) external {
        for (uint256 index = 0; index < nums.length; index++) {
            bool isEven = nums[index] % 2 == 0;
            bool isLessThan99 = nums[index] < 99;
            if (isEven && isLessThan99) {
                total += nums[index];
            }
        }
    }
}
/**
  * 默认: 50518 gas
  */

优化后

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

contract Gas {
    uint256 public total;

    // [1,2,3,4,5,100]
    function demo(uint256[] calldata nums) external {
        uint256 _total = total;
        uint256 len = nums.length;
        for (uint256 index = 0; index < len; ++index) {
            uint256 num = nums[index];
            if (num % 2 == 0 && num < 99) {
                _total += num;
            }
        }
        total = _total;
    }
}

/**
 * 默认 => 50518 gas
 * 1. 函数参数不使用 memory,改用 calldata
 *      => 48773    节省了 1745
 * 2. 状态变量在函数内不每次都读取和修改,缓存到内存里,统一修改
 *      => 48562    节省了 211
 * 3. 短路(条件 &&)
 *      => 48244    节省了 318
 * 4. 循环增量 i++ 改为 ++i
 *      => 48214    节省了 30
 * 5. 循环时,缓存数组的长度 uint256 len = nums.length;
 *      => 48179    节省了 35
 * 6. 数组的元素,提前缓存,不重复读取
 *      => 48017    节省了 162
 *
 */