优化 Solidity 中的气体
在本教程中,我们将学习 Solidity 中的一些气体优化技术。这是我们要求最多的一个级别,所以让我们开始吧,不要再多说了。
技巧和窍门
可变包装
如果你还记得我们在之前的一个关卡中谈到了存储槽。现在,如果你记得的话,solidity 中有趣的一点是,每个存储槽是 32 字节。
如果你正确地打包你的变量,这个存储槽可以被优化,这将进一步意味着你在部署你的智能合约时的气体优化。
打包你的变量意味着你将较小尺寸的变量打包或放在一起,使它们共同形成 32 个字节
。例如,你可以将 32 个 uint8
打包到一个存储槽中,但要做到这一点,你必须连续声明它们,因为变量的声明顺序在 solidity 中很重要。
给出两个代码样本。
uint8 num1;
uint256 num2;
uint8 num3;
uint8 num4;
uint8 num5。
uint8 num1;
uint8 num3;
uint8 num4;
uint8 num5。
uint256 num2。
第二种情况更好,因为在第二种情况下,solidity 编译器会把所有的 uint8 放在一个存储槽中,但在第一种情况下,它会把 uint8 num1 放在一个槽中,但现在它看到的下一个是 uint256,它本身需要 32 字节,因为 256/8 比特=32 字节,所以它不能和 uint8 num1 放在同一个存储槽中,所以现在它将需要另一个存储槽。之后,uint8 num3, num4, num5 将被放在另一个存储槽中。因此,第二个例子需要 2 个存储槽,而第一个例子则需要 3 个存储槽。
还需要注意的是,内存中的元素和 calldata 不能被打包,也不能被 solidity 的编译器优化。
存储与内存
改变存储变量比改变内存中的变量需要更多的气体。最好是在所有的逻辑都已经实现之后,在最后更新存储变量。
因此,给定两个代码的样本
contract A {
uint public counter = 0;
function count() {
for(uint i = 0; i < 10; i++) {
counter++;
}
}
}
contract B {
uint public counter = 0;
function count() {
uint copyCounter;
for(uint i = 0; i < 10; i++) {
copyCounter++;
}
counter = copyCounter;
}
}
第二段代码样本更加气体优化,因为我们只向存储变量 counter 写了一次,而第一段代码样本是在每个迭代中向存储写。尽管我们在第二个代码样本中多写了一次,但向内存写 10 次,向存储写 1 次,仍然比直接向存储写 10 次便宜。
固定长度和可变长度的变量
我们谈到了固定长度和可变长度变量的存储方式。固定长度的变量存储在 stack 中,而可变长度的变量则存储在 heap 中。
从本质上讲,为什么会出现这种情况,是因为在堆中,你确切地知道在哪里可以找到一个变量和它的长度,而在堆中,鉴于变量的可变性,会有额外的遍历成本。
因此,如果你能使你的变量固定大小,这对气体优化总是有好处的。
给出两个代码的例子。
string public text = "Hello";
uint[] public arr;
bytes32 public text = "Hello";
uint[2] public arr;
第二个例子更加气体优化,因为所有的变量都是固定长度的。
外部、内部和公共函数
在 solidity 中调用函数可能是非常耗气的,与其调用多个函数,不如调用一个函数并从中提取所有数据。
回想一下,公共函数是那些既可以在外部(由用户和其他智能合约)又可以在内部(从同一合约的另一个函数)调用的函数。
然而,当你的合约正在创建只被外部调用的函数时,这意味着合约本身不能调用这些函数,你最好使用外部关键字而不是公共关键字,因为公共函数中的所有输入变量都被复制到内存中,这需要耗费气体,而对于外部函数,输入变量被存储在 calldata 中,这是一个用于存储函数参数的特殊数据位置,存储在 calldata 中需要的气体比存储在内存中要少。
同样的原理也适用于为什么调用内部函数比调用公共函数更便宜。这是因为当你调用内部函数时,参数是作为变量的引用传递的,不会再被复制。
函数修改器
这是一个令人着迷的问题,因为几周前,我正在调试我们一个学生的这个错误,他们遇到了 "堆栈太深 "的错误。这通常发生在你在函数中声明了大量的变量,而该函数的可用堆栈空间已经无法使用。正如我们在 Ethereum 存储层面所看到的,EVM 只允许在一个函数中最多有 16 个变量,因为它不能在堆栈中执行超过 16 层深度的操作。
现在,即使在修改器中移动了很多 require 语句,也无济于事,因为函数修改器使用的堆栈与它们所处的函数相同。为了解决这个问题,我们在修改器中使用了一个内部函数,因为内部函数不与原始函数共享相同的限制性堆栈,但修改器却可以。