{ 跳至内容 }

通过简单内联节省 Gas

作者:Christian Reitwiessner,发表于 2021 年 3 月 2 日

解释器

Solidity v0.8.2 在 Solidity 的低级优化器 中添加了一个简单的内联器。在这篇文章中,我们将探讨它的工作原理,并了解它与优化器的其他步骤的协同作用。

低级内联器

低级内联器是 Solidity 编译器低级优化器的一部分。为了节省 Gas,它可以内联不包含控制流分支或具有副作用的操作码的短函数。

是否内联的决定基于折衷参数 "runs":假设代码执行 "runs" 次,计算组合的代码存款成本和执行成本。如果估计内联版本整体比非内联版本更便宜,则内联函数调用。

特别是,优化器分析以下形式的代码:

PUSH <tag>
JUMP
...
<tag>:
[ROUTINE]
JUMP

如果操作码序列[例程] 足够短且简单,则第一个 JUMP 将被此代码的副本替换,从而产生以下结果

[ROUTINE]
JUMP
...
<tag>:
[ROUTINE]
JUMP

如果以这种方式消除了对该标签的所有引用,那么该标签本身和原始例程也可以被删除,从而节省更多 Gas。

逐步演练和与其他优化器步骤的协同作用

你可能会问,对于 [例程] 而言,"简单" 意味着什么,以及为什么它必须以 JUMP 操作码结束。这种限制背后的理念是,我们希望内联器只是通往进一步优化的第一步。

看看以下代码

function unsafeAdd(uint x, uint y) pure returns (uint) {
    unchecked { return x + y; }
}
function doSomthing(uint x) pure returns (uint) {
    ...
    uint z = unsafeAdd(x, 7);
    ...
}

我们当然希望对 unsafeAdd 的调用被内联。在内部,Solidity 将函数调用粗略地转换为以下汇编代码

...
PUSH <returnTag>
PUSH 7
DUP3 // fetch x
PUSH <unsafeAdd>
JUMP
returnTag:
...

unsafeAdd:
ADD
SWAP1
JUMP

对于调用,堆栈如下所示(堆栈顶部在右侧)

<return address> <y> <x>

因此,代码中的最后一个 JUMP 跳回到调用点。

内联后,代码如下所示

...
PUSH <returnTag>
PUSH 7
DUP3 // fetch x
ADD
SWAP1
JUMP
returnTag:
...

unsafeAdd:
ADD
SWAP1
JUMP

Solidity 编译器中还有一个名为 "Common Subexpression Eliminator" 的优化器阶段。尽管它的名字很奇怪,但它实际上是一个符号推理引擎,它将代码转换为内部表示,简化它并尝试生成语义相同但指令更少的代码。该阶段注意到 PUSH <returnTag> 在堆栈上一直闲置到最后(它被 JUMP 消耗)并以以下方式重新排列代码

...
PUSH 7
DUP2 // fetch x
ADD
PUSH <returnTag>
JUMP
returnTag:
...

unsafeAdd:
ADD
SWAP1
JUMP

现在代码处于一种形式,其中可以确定 JUMP 操作码的目标,而无需进行可能代价高昂的堆栈分析,因为它被推到了操作码的正上方。此外,它跳到一个只是下一个操作码的标签。这是一个完美的时机,可以利用优化器中的另一个阶段,称为 "Peephole Optimizer":它试图找到操作码序列的简单模式,而无需进行完整的语义或符号分析。它将删除 "跳到下一个" 操作码三元组,并将代码变成这样

...
PUSH 7
DUP2 // fetch x
ADD
...

unsafeAdd:
ADD
SWAP1
JUMP

当然,最后,有一个不可达代码删除器可以消除 "unsafeAdd" 例程(除非它从其他地方被引用)

...
PUSH 7
DUP2 // fetch x
ADD
...

现在回到我们为什么要求例程必须简单的点:一旦你做更多复杂的事情,例如分支、调用外部合约,Common Subexpression Eliminator 就无法再重新构建代码,或者它不会对表达式进行完全的符号评估。此外,它只有在最后有一个 JUMP 的情况下才能完全内联函数。

结论和未来展望

在我们的代码库中,这个简单的例程能够降低许多测试的 Gas 成本。为了防止错误,我们始终努力使各个优化器阶段尽可能简单,以便它们的全部潜力主要在与其他阶段的组合中实现。由于它位于低级优化器中,这个新的内联器可以实现高级内联器无法实现的优化机会,因为它不操作单个跳转级别,也不能拆分函数。

在此例程中,我们有一个缺点,在实现之前我们仔细考虑了它:因为它可以拆分函数,所以它可能会导致调试器对函数的开始位置和实际调用位置感到困惑 - 甚至可能导致整个函数被删除,这实际上是这里的要点。

随着越来越多的激进优化,智能合约的可调试性正在下降。这可以通过编译器维护调试信息来解决,调试信息与优化器步骤一起转换并导出以帮助分析。这是我们将来要解决的一个任务。

上一篇

下一篇

参与进来

GitHub

Twitter

Mastodon

Matrix

了解更多

博客文档用例贡献关于论坛

2024Solidity 团队

安全策略

行为准则