在web2,只要功能实现了,代码写的好不好,最终用户是无法知道的。但是合约程序员就没这么幸福,代码写的不够,gas可能就非常高,导致的结果就是用户要多花钱。谁调用谁花钱,谁花钱谁知道。既然gas代表着明晃晃的钱,那我们就研究下怎么才能少花钱。本文用到的代码完整版:https://github
在web2,只要功能实现了,代码写的好不好,最终用户是无法知道的。
但是合约程序员就没这么幸福,代码写的不够,gas可能就非常高,导致的结果就是用户要多花钱。谁调用谁花钱,谁花钱谁知道。既然gas代表着明晃晃的钱,那我们就研究下怎么才能少花钱。
<br>
本文用到的代码完整版:
<https://github.com/rickwang9/GasOptimization>
<br>
打开 remix,新建一个合约Gas1.sol,写两个非常简单的函数:
function lowGas() public view returns(uint) {
uint x=10;
x = x + x;
return x;
}
function highGas() public view returns(uint) {
uint x=10;
x = x * 2;
return x;
}
两个函数都非常简单,但是消耗的gas却不一样!!! <br>
空口无凭,我需要实现逻辑,把调用每个函数消耗的gas都清晰的打印出来。
下面定义一个修改器modifier costGas
modifier costGas(string memory name) {
uint beforeGas = gasleft();
_;
uint afterGas = gasleft();
console.log(name, concat("costGas: ", Strings.toString(beforeGas - afterGas)));
}
核心是 gasleft(),返回值是uint,表示代码执行到这里,还剩多少gas。
<br>
另外,我还需要几个辅助方法
console.log, solidity本身不支持打印,需要调用 hardhat的包,可以通过import导入
import "hardhat/console.sol";
<br>
concat,EVM对字符串的支持度不能说没有,只能非常低。原因以后说。像字符串的拼接,在其他语言都是最常见的,在solidity需要自己实现。
function concat(string memory value1, string memory value2) public pure returns(string memory result){
result = string(abi.encodePacked(value1, value2));
}
<br>
Strings.toString(), solidity不支持字符串拼接,自然也不会支持整形转为字符串。还好有OpenZeppelin
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.0/contracts/utils/Strings.sol";
<br>
准备好了这些,就可以打印前面两个函数的gas费用了, 修改代码,每个函数加上修改器constGas,传参是为了打印信息清晰一些:
function lowGas() costGas('lowGas') public view returns(uint) {
uint x=10;
x = x + x;
return x;
}
function highGas() costGas('highGas') public view returns(uint) {
uint x=10;
x = x * 2;
return x;
}
相信大家已经迫不及待看到运行结果了。别着急,耽误一点时间,看看import hardhat的openzeppelin都在哪里?看下图:
<br>
好了,可以执行代码了。 <br>
执行lowGas函数, 返回20,正确
gas消耗 222
CALL
[call]from: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4to: Ga1.lowGas()data: 0x895...b3373
console.log:
lowGas costGas: 222
<br>
执行highGas函数, 返回20,正确
gas消耗 285
CALL
[call]from: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4to: Ga1.highGas()data: 0x62d...12eff
console.log:
highGas costGas: 285
<br>
打开 evm.codes, 这里列举了所有的opCode的gas fee
可以清晰的看到, 加法gas=3, 乘法gas=5。
*所以: gas(10 + 10) < gas(10 2)**
<br>
等等, 是不是有点有点不对? lowGas:285, highGas:222, diff=63 ,而不是 5-3=2。
<br>
因为solc会把solidity编程成bytecode, EVM根据栈执行各种指令,来运行程序。 一行简单的计算,bytecode可能就是push,dup,swap,if,pop等等指令的组合。为了保证安全,bytecode包含了很多安全检查的指令,这些检查在你的代码里根本没有,不是你写的。但是bytecode里有,编译器自己加上的。
<br>
生成的bytecode,往往复杂重复且繁琐。所以想把复杂的bytecode反编译回源码,非常困难。
<br>
依然有很多多余的指令被solc生成,我们能否优化一下。答案是能。
<br>
看图,分成两步:
1) 编译界面,展开“>”符号
<br>
2) 允许优化
勾选Enable optimization
输入 200
解释一下,200 告诉优化器这个合约会被调用多少次。200表示多次调用,它就会优化bytecode,让本次调用的更节省gas。
<br>
我测试过:输入 200和3000,发现效果是一样的。uniswap的合约部署也是输入200.
<br>
然后看一下优化结果:
gas消耗 94
CALL
[call]from: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4to: Ga1.lowGas()data: 0x895...b3373
console.log:
lowGas costGas: 94
lowGas 返回20,正确
gas消耗 110
CALL
[call]from: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4to: Ga1.highGas()data: 0x62d...12eff
console.log:
highGas costGas: 110
<br>
优化前 | |
---|---|
lowGas | 222 |
highGas | 285 |
diff | 63 |
优化后 | |
---|---|
owGas | 94 |
highGas | 110 |
diff | 16 |
虽然 diff 依然没有达到2, 但是已经很好了。 <br>
solc生成的bytecode不可能和我们的代码完全一样,bytecode内部一定包含了solc认为有用的检查代码和必要的出入栈操作。 <br>
我们这里可以得出一个结论:
<br> <br>
<!--StartFragment-->
本文用到的代码完整版:
https://github.com/rickwang9/GasOptimization
<!--EndFragment-->
<!--StartFragment-->
<https://learnblockchain.cn/column/1>
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!