Solidity 优化 - 隐藏的 Gas 成本
图文来自:omniscia.io
本文将研究以太坊虚拟机(EVM)的内部工作,以说明如何 "利用 "EVM的特殊特性,为用户最小化solidity智能合约的执行成本。社区发布许多关于 solidity开发者可以利用的知识来设计和开发更安全、更节省Gas的智能合约。
本周我们看一下两种不同类型的优化,这些优化相对简单,可以应用于任何代码库,与我们为客户审计的大多数智能合约有关。第一个优化适用于所有版本的 Solidity。本文讨论的第二个优化只对pragma
版本0.8.0
以上有效。然而,让我们首先在高层次上阐明如何评估任何EVM指令的成本。
最核心的是,在EVM区块链上为交易引入 "Gas 成本"和为组装区块引入 "Gas限制 "的理由是:1)引入额外的收入流,以激励stakers(以前的矿工)确保和验证网络,以及2)作为保护措施,防止拒绝服务(DoS)攻击,通过Gas上限禁止执行计算昂贵的任务(例如,具有显著限制的 "for "循环,否则会延迟区块创建率的中位数)。
为了在区块构建者补偿和有竞争力的计算系统之间取得余额,EVM区块链根据网络行为者之间的带宽需求动态地调整其区块Gas限制。交易Gas成本通常发展缓慢,围绕着一个简单的基础;执行交易的计算成本。在中心化和/或传统的计算环境中,这可能看起来很简单。然而,在区块链生态系统中,由于区块链账本的状态变化在验证去中心化网络上执行的指令的节点网络中传播的方式,它有很大不同。
在这篇文章中,我们说明了 "内存" 的隐性成本如何抬高了EVM区块链上其他直接交易类型的成本,以及开发者如何优化他们的dapps以减少其Gas足迹。
当声明一个作为语句结果的局部变量时,会产生一个隐藏的Gas成本,它与我们声明的局部变量所需的 内存
量成比例。当从存储空间读取变量(SLOAD
),将它们存储到局部变量(参考 MSTORE
内存扩展 增加了隐藏成本),以及在每次利用时读取它们(MLOAD
)时,这种额外成本通常会被抵消。
在处理EVM的原始指令时,情况就不是这样了。事实上,区块链上的每笔交易都包含一组不可避免的数据。因此,数据集通过原始的EVM指令暴露给所有智能合约,这些指令消耗的Gas 非常小。这是由于它们不需要额外的内存来读取特殊的数据槽,因为它们已经作为EVM的区块创建工作流程的一部分在内存中加载。
这些指令集很重要,他们围绕着交易的上下文数据,如msg.sender
,block.timestamp
,等等。因此,下面的合约实现事实上是低效的。
Aave v3的 "Context.sol"中的片段 @ f3e037b
Context 合约是OpenZeppelin引入的一个实现,目的是简化合约的开发过程,可以很容易地升级到元交易兼容的合约。然而,到目前为止,它大多被滥用,并导致各种协议(包括Aave V2和Aave V3)的Gas增加到不可忽略的程度,这是Aave V3的 "IncentivizedERC20 "实现的具体例子:
来自Aave v3的 "IncentivizedERC20.sol "的片段 @ f3e037b
在上述函数中,gas 成本包含:_msgSender
实现的 msg.sender
Gas成本(操作码 CALLER: 2 gas
),以及_msgSender()
调用本身(操作码 JUMP: 2gas
以及返回变量的内存分配)两次。通过优化上述片段,我们可以将指令的Gas成本降低一半:
来自Aave v3的 "IncentivizedERC20.sol "的优化片段 @ f3e037b
虽然这种优化本身可能微不足道,但在整个代码库中应用时,它将带来切实的节省。
隐性Gas成本不仅限于EVM。开发人员需要认识到,Solidity语言本身在其最新版本semver 8
中引入了一些隐性成本,Solidity默认执行安全算术。鉴于很多应用程序已经对不安全的算术操作进行了安全检查,作为其错误处理工作流程的一部分,内置的安全算术检查变得多余了,因此会产生多余的Gas增加。
值得庆幸的是,Solidity还引入了一种新的代码块声明风格,指示编译器不安全地执行算术操作。unchecked
代码块。只要操作被周围的语句和/或条件保证安全执行,就可以巧妙地利用这些代码块来大大减少特定合约的Gas成本。作为一个例子,让我们看一下复合CToken
实现的_reduceReservesFresh
函数的这一段:
来自Compound的 "CToken.sol "的片段 @ a3214f6
在条件 reduceAmount > totalReserves
被评估为 false
之后,totalReservesNew
的计算和分配被执行。这意味着执行环境已经保证了 "totalReserves >= reduceAmount "这一特性,因此 "totalReservesNew "的计算可以在一个unchecked
的代码块中进行,因为它被保证能够正常执行。经过优化,上述代码块应该类似于这样:
来自 "CToken.sol "的Compound @ a3214f6的优化摘录
另一种避免内置安全算术产生额外Gas的方法是在增量操作(++
和--
)期间使用unchecked
。通常在for
循环和任何0.8.X
后的版本中进行操作都有此问题。每一次增量操作都会进行边界检查,当它们完全是多余的。下面提供一个非常简单的例子来说明这一点:
循环的例子片段
由于Solidity的固有限制,bar.length
被保证适合于uint256
变量,这意味着对i
变量的每个循环执行安全增量将是多余的。为了优化这样的代码块,我们把增量移到 unchecked
的代码块的末尾:
优化后的循环实例片段
EVM是一个内在复杂的机器,因此已经开发了多种工具来帮助开发者使用高级语言(如Solidity)在其系统中创建解决方案。然而,在Solidity编译器的自由下进行的简化,通常没有很好地转达给开发者社区,因此,程序员最终创造了低效的程序。
欢迎订阅我们的专栏:全面掌握Solidity智能合约开发 及 理解 EVM - 探究Solidity 背后的秘密 学习如何写出高效的合约代码。
本翻译由 Duet Protocol 赞助支持。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!