现在写合约中有很多方法可以节省 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.
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,在一些需要高频读写的场景将函数外部变量拷贝到函数内部操作是一个不错的方法。
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 的,某些时候可以适当减少内部声明变量。
循环内部没错都会声明isEven
、isLessThan99
2 个变量,这歌变量只是用来做一次条件判断,明显是可以合并起来,减少内部声明的变量。
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++ 语言特性有关
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。
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;
}
取消溢出检查
Solidity 8.0 之后默认会对数字做益处检查 ,默认就会消耗一定 Gas 可以加 Unchecked
来取消检查,某些情况可以省很多 Gas。
function sumIfEvenAndLessThan99(uint256[] calldata nums) external {
uint256 _total = total;
uint256 len = nums.length;
for (uint256 i = 0; i < len; ) {
uint256 num = nums[i];
if (num % 2 == 0 && num < 99) {
unchecked {
_total += num;
}
}
unchecked {
++i;
}
}
unchecked {
total = _total;
}
}
总结
统计了下每个修改之后和最开始相比节省的 Gas。
操作 | Gas | 节约 Gas |
---|---|---|
初始 | 76309 | 0 |
参数从 memory 改为 calldata | 74334 | 0 |
循环内高频写入的状态变量拷贝到内存中 | 56663 | 0 |
合并判断条件,减少内部变量 | 55477 | 0 |
循环自增变量优化 | 55419 | 0 |
将数组元素加载到内存 | 55426 | 0 |
将数长度加载到内存 | 54475 | 0 |
unchecked | 52442 | 0 |
计算 Gas 其实只有 2 个输入参数 时间
、空间
, 写合约的时候也只是对这 2 个输入做权衡、取舍。最近 10 年云计算的迅猛发展,虽然单一硬件性能已经挤不动牙膏了,但是可以大规模堆计算资源,所以并未如此细致的计算时间,空间。
参考
- Gas Saving Techniques - Solidity by Example
- Gas Costs from Yellow Paper - 黄皮书中每个操作的 Gas