智能合约Solidity的Gas消耗(Gas优化)总结

智能合约Solidity操作消耗gas级别(从小到大排序,以256位的整型状态变量为例,经Remix初略测试):

  • 内部调用view/pure的fun() 消耗:几十gas
  • 内部调用写状态变量的fun() 消耗:几百gas
  • 外部调用view/pure的fun() 消耗:两三千gas
  • 外部调用写状态变量的fun() 消耗:三四千gas
  • 写一个uint状态变量 消耗:两万多gas (用uint8代替uint256反而消耗gas更多)
  • 修改一个定长数组(状态变量)元素 消耗:两万多gas
  • 插入一个map(状态变量)元素 消耗:两万多gas
  • push一个uint[]变长数组(状态变量)元素 消耗:四万多gas

结论

1、勇敢地使用uint256吧,少很多类型转换和兼容性问题,uint8消耗更多gas呢(意外!)

2、状态变量存储:序列数据尽量用map或者定长数组,而不是变长数组;

3、只读操作采用静态call模式调用,尽量减少外部调用。

如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pragma solidity ^0.7.6;


contract Demo{

struct Margin {
uint256 marginBalance;
uint256 marginBalance1;
uint256 marginBalance2;
uint256 marginBalance3;
uint256 marginBalance4;
uint256 marginBalance5;
}
mapping (address => Margin) public marginAccounts;

function setInMap (uint256 num) public {
Margin memory account = marginAccounts[msg.sender];
set(account,num);
marginAccounts[msg.sender] = account;
}

function set(Margin memory account,uint256 num) internal{
account.marginBalance = num;
account.marginBalance1 = num;
account.marginBalance2 = num;
account.marginBalance3 = num;
account.marginBalance4 = num;
account.marginBalance5 = num;
}
}

当marginAccounts[msg.sender]对应对象的属性为0时,执行setInMap方法消耗150000左右gas,而当状态变量不为0的时候。执行setInMap方法仅需消耗50000左右gas。结论如下:

  • 修改非0的状态变量比修改为0的状态变量消耗的gas更少;
  • 将一个状态变量设置为0将比修改非0的状态变量消耗的gas更少。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct Data1 {
uint64 a;
uint64 b;
uint128 c;
uint256 d;
}

struct Data2 {
uint64 a;
uint256 d;
uint64 b;
uint128 c;
}

Data1 public data1;
Data2 public data2;
uint256 public gasUsed1;
uint256 public gasUsed2;
function set1(uint64 _a, uint64 _b, uint128 _c, uint256 _d) public{
uint256 start = gasleft();
data1.a = _a;
data1.b = _b;
data1.c = _c;
data1.d = _d;
gasUsed1 = start - gasleft();
}

function set2(uint64 _a, uint64 _b, uint128 _c, uint256 _d) public{
uint256 start = gasleft();
data2.a = _a;
data2.b = _b;
data2.c = _c;
data2.d = _d;
gasUsed2 = start - gasleft();
}

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