关于gas费优化问题
首先我们先来看一下这段代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GasGolf{
uint public total;
//[1,2,3,4,5,100]
function sum(uint[] memory nums) external{
for(uint i = 0;i<nums.length;i+=1){
bool isEven = nums[i] % 2 == 0;
bool isLessThan99 = nums[i] < 99;
if(isEven && isLessThan99){
total += nums[i];
}
}
}
}
在这段代码中定义了一个total的状态变量,并且将其传入数组的偶数以及小于99的数去进行累加。
当我们一开始运行sum方法的时候,我们可以看出第一次执行的gas费用为
28654
现在我们开始第一个优化:将memory改为calldata
contract GasGolf{
uint public total;
//[1,2,3,4,5,100]
function sum(uint[] calldata nums) external{
for(uint i = 0;i<nums.length;i+=1){
bool isEven = nums[i] % 2 == 0;
bool isLessThan99 = nums[i] < 99;
if(isEven && isLessThan99){
total += nums[i];
}
}
}
}
此时我们再看看所需要用到的gas费用,此时少了2000左右
26909
为什么会这样呢?
我们对比一下calldata和memory的区别
在Solidity中,calldata
是指函数调用参数的存储位置。而memory
是在函数内部声明的临时变量的存储位置。
使用calldata
比memory
更能节省gas费用的原因是,calldata
是只读的,在函数执行期间不能被修改。相反,memory
中的数据可以在函数执行期间被修改,这意味着如果你使用memory
存储大数据结构,每次修改都需要消耗更多的gas费用。
在这个场景中我们可以看到似乎nums作为一个入参似乎并不需要被改变,所以这里我们直接用calldata即可。
我们再来看看循环体内部
我们发现这里面的total这个状态变量,在每一次循环里面都会将total里面的重新赋值,这种在整体状态变量层面的操作实际上是非常浪费gas的。
我们不妨换一种思路,将total抽离到循环外,方法内单独设置一个变量去将total拷贝到内存中去,也就是我们每次累加的是内存中的一个变量,这样并不会写入状态变量。最后再循环结束后再一次性的将结果写入到状态变量中。
代码如下:
contract GasGolf{
uint public total;
//[1,2,3,4,5,100]
function sum(uint[] calldata nums) external{
uint _total = total;
for(uint i = 0;i<nums.length;i+=1){
bool isEven = nums[i] % 2 == 0;
bool isLessThan99 = nums[i] < 99;
if(isEven && isLessThan99){
_total += nums[i];
}
}
total = _total;
}
}
此时我们可以执行看看gas费用的消耗,此时又节省了200左右
26698
我们再观察一下里面还有什么地方可以优化的
不难看出里面其实isEven和isLessThan99其实没必要单独给他每一次都赋值,我们直接给他合并到条件判断里面就好,合并后的效果如下:
contract GasGolf{
uint public total;
//[1,2,3,4,5,100]
function sum(uint[] calldata nums) external{
uint _total = total;
for(uint i = 0;i<nums.length;i+=1){
if(nums[i] % 2 == 0 && nums[i] < 99){
_total += nums[i];
}
}
total = _total;
}
}
此时gas费用又节省了300左右
26380
我们再看看for(uint i = 0;i<nums.length;i+=1)这一段代码,i+=1实际上会将 i 的值复制到一个临时变量中,然后对 i 进行递增操作,最后再将临时变量的值返回给表达式。那有没有可以避免创建临时变量的方法呢?答案是有的:我们只需要改为++i即可
contract GasGolf{
uint public total;
//[1,2,3,4,5,100]
function sum(uint[] calldata nums) external{
uint _total = total;
for(uint i = 0;i<nums.length;++i){
if(nums[i] % 2 == 0 && nums[i] < 99){
_total += nums[i];
}
}
total = _total;
}
}
此时的gas费用又节省了300多
26008
同样是刚才的循环,我们可以看出每一次循环中都要读取出数组的长度,这样每次循环都要执行这个方法,那么既然他的长度是不变的,我们不如直接给他存入缓存变量中。代码如下:
contract GasGolf{
uint public total;
//[1,2,3,4,5,100]
function sum(uint[] calldata nums) external{
uint _total = total;
uint len = nums.length;
for(uint i = 0;i<len;++i){
if(nums[i] % 2 == 0 && nums[i] < 99){
_total += nums[i];
}
}
total = _total;
}
}
此时gas费用又省了100多
25973
再看看循环内部的代码,我们针对循环体内数组的元素nums[i]其实可以提前复制到内存中,代码如下:
contract GasGolf{
uint public total;
//[1,2,3,4,5,100]
function sum(uint[] calldata nums) external{
uint _total = total;
uint len = nums.length;
for(uint i = 0;i<len;++i){
uint num = nums[i];
if(num % 2 == 0 && num < 99){
_total += num;
}
}
total = _total;
}
}
此时gas费用又节省了200左右
25811