浅谈 Solidity Gas 优化

  • kyleliu
  • 更新于 2022-05-16 14:00
  • 阅读 4801

现在写合约中有很多方法可以节省 Gas, 这里发现一个不错案例比较循序渐进,可以参考。

原文地址

现在写合约中有很多方法可以节省 Gas, 这里发现一个不错案例比较循序渐进,可以参考。

案例

一个计算输入数组中偶数之和的方法,并记录计算之后的结果。

输入恒定 [12, 3, 4, 5, 3, 44, 2, 12, 3, 4, 5, 21, 46, 1, 2, 12]

初始 Code 未优化

uint public total;
function sumIfEvenAndLessThan99(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];
        }
    }
}

只读参数使用 calldata

Solidity 变量中 memory 、calldata 2 个表示作用非常类似,都是函数内部临时变量,它们最大的区别就是 calldata 是不可修改的,在某些只读的情况比较省 Gas.

参数从 memory 改为 calldata - 51858

uint public total;
function sumIfEvenAndLessThan99(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];
        }
    }
}

高频读写参数拷贝到函数内部

Solidity 函数也是类似栈到结构,对函数内部变量的读写要比读写外部变量省 Gas,在一些需要高频读写的场景将函数外部变量拷贝到函数内部操作是一个不错的方法。

循环内高频写入的状态变量拷贝到内存中 - 51428

uint public total;
function sumIfEvenAndLessThan99(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;
}

减少变量声明次数

Solidity 声明的内存是要算 Gas 的,某些时候可以适当减少内部声明变量。 循环内部没错都会声明isEvenisLessThan992 个变量,这歌变量只是用来做一次条件判断,明显是可以合并起来,减少内部声明的变量。

合并判断条件,减少内部变量 - 50870

uint public total;
function sumIfEvenAndLessThan99(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;
}

特殊自增优化

这个似乎和 c++ 语言特性有关

循环自增变量优化 - 50258

uint public total;
function sumIfEvenAndLessThan99(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;
}

将高频读取的参数拷贝的函数内部

数组长度nums.length 与数组循环变量 nums[i]每次循环都会读,而且是从参数中读,可以拷贝到内存当中更加节约 Gas。

将数组元素加载到内存 - 50028

uint public total;
function sumIfEvenAndLessThan99(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。

操作 Gas 节约 Gas
初始 63964 0
参数从 memory 改为 calldata 59897 4067
循环内高频写入的状态变量拷贝到内存中 58219 5745
合并判断条件,减少内部变量 57358 6606
循环自增变量优化 56315 7649
将数组元素加载到内存 55636 8328
将数长度加载到内存 55543 8421

计算 Gas 其实只有 2 个输入参数 时间空间, 写合约的时候也只是对这 2 个输入做权衡、取舍。最近 10 年云计算的迅猛发展,虽然单一硬件性能已经挤不动牙膏了,但是可以大规模堆计算资源,所以并未如此细致的计算时间,空间。

参考

点赞 3
收藏 9
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
kyleliu
kyleliu
江湖只有他的大名,没有他的介绍。