- 使用短路模式来排序操作
- 函数使用
external
+calldata
- 对
memory
类型的参数,使用external
并且参数标注calldata
是最省钱的。
- 对
- 使用正确的数据类型
- gas 由小到大: 值类型
<
引用类型
- 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.17;
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;
}
}
在合约开发种,显式声明函数的可见性不仅可以提高智能合约的安全性,同时也有利于优化合约执行的 gas 成本。
例如,通过显式地标记函数为外部函数 external
,可以强制将函数参数的存储位置设置为 calldata,这会节约每次函数执行时所需的以太坊 gas 成本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
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.17;
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.17;
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.17;
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 变量的 gas 成本要远远高于内存变量,所以要避免在循环中操作 storage 变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
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.17;
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.17;
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.17;
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.17;
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.17;
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
*
*/