EIP-5656: MCOPY - 内存复制指令
一种用于复制内存区域的高效 EVM 指令
Authors | Alex Beregszaszi (@axic), Paul Dworzanski (@poemm), Jared Wasinger (@jwasinger), Casey Detrio (@cdetrio), Pawel Bylica (@chfast), Charles Cooper (@charles-cooper) |
---|---|
Created | 2021-02-01 |
摘要
提供一种用于复制内存区域的高效 EVM 指令。
动机
内存复制是一项基本操作,但在 EVM 上实现它会产生开销。
这一点很早就被认识到,并通过引入“identity”预编译得到缓解,该预编译通过使用 CALL
的输入和输出内存偏移量来完成内存复制。它的成本是 15 + 3 * (length / 32)
gas,加上调用开销。“identity”预编译因 CALL
的成本提高到 700 而失效,但随后通过 EIP-2929 的降低使其略微经济。
复制精确的字可以用 <offset> MLOAD <offset> MSTORE
或 <offset> DUP1 MLOAD DUP2 MSTORE
来完成,每个字的成本至少为 12 gas。如果偏移量是预先知道的,并且复制可以展开,这相当有效。如果复制是在运行时以任意起始偏移量实现的,除了控制流开销外,还需要使用 32 ADD
来增加偏移量,每个字至少增加 6 gas。
复制非精确的字更加棘手,因为对于最后一个部分字,源和目标都需要加载、屏蔽、或运算并再次存储。这种开销是巨大的。一个边缘情况是,如果最后一个“部分字”是单个字节,则可以使用 MSTORE8
有效地存储它。
作为用例示例,复制 256 字节的成本:
- 在使用 identity 预编译的 EIP-2929 之前至少 757 gas
- 在使用 identity 预编译的 EIP-2929 之后至少 157 gas
- 使用展开的
MLOAD
/MSTORE
指令至少 96 gas - 使用此 EIP 为 27 gas
根据对区块 10537502 到 10538702 的分析,大约 10.5% 的内存复制可以通过 MCOPY
指令的可用性来提高性能。
内存复制被 Solidity 和 Vyper 等语言使用,我们希望这种改进能够提供构建数据结构的高效方法,包括高效的切片访问和内存对象的复制。拥有专用的 MCOPY
指令还可以防止未来对 CALL
指令 gas 成本的更改。
拥有一个特殊的 MCOPY
指令可以使静态分析器和优化器的工作更容易,因为通常必须限制 CALL
的效果,而 MCOPY
指令仅具有内存效果。即使为预编译添加了特殊情况,未来的硬分叉也可能会更改 CALL
效果,因此对使用 identity 预编译的代码的任何分析都仅对特定范围的区块有效。
最后,我们希望内存复制对于各种计算密集型操作非常有用,例如 EVM384,其中它被认为是一个重要的开销。
规范
指令 MCOPY
在 0x5E
处引入。
输入栈
栈 | 值 |
---|---|
top - 0 | dst |
top - 1 | src |
top - 2 | length |
此顺序与其他复制指令(即 CALLDATACOPY
、RETURNDATACOPY
)相匹配。
Gas 成本
按照黄皮书的术语,它应被视为 W_copy
操作码组的一部分,并遵循黄皮书中 W_copy
的 gas 计算。虽然黄皮书中的计算应被视为最终版本,但作为参考,截至撰写本文时,这目前意味着其 gas 成本为:
words_copied = (length + 31) // 32
g_verylow = 3
g_copy = 3 * words_copied + memory_expansion_cost
gas_cost = g_verylow + g_copy
输出栈
此指令不返回任何栈项。
语义
它将 length
字节从 src
指向的偏移量复制到内存中 dst
指向的偏移量。
复制的发生就好像使用了中间缓冲区一样,允许目标和源重叠。
如果 length > 0
并且 (src + length
或 dst + length
) 超出当前内存长度,则内存会扩展并应用相应的 gas 成本。
此指令的 gas 成本与其他 Wcopy
指令的 gas 成本相同,为 Gverylow + Gcopy * ceil(length / 32)
。
原理
可以在 Solidity、Vyper 和 Fe 编译器中找到精确字内存复制和部分字内存复制的生产实现。
通过 EIP-2929,使用 identity 预编译的调用开销从 700 降低到 100 gas。 这对于使预编译再次成为合理的替代方案仍然是令人望而却步的。
向后兼容性
此 EIP 引入了一个以前不存在的新指令。已经部署的使用此指令的合约可能会在此 EIP 之后更改其行为。
测试用例
MCOPY 0 32 32
- 将 32 字节从偏移量 32 复制到偏移量 0。
之前(包含空格以提高可读性):
0000000000000000000000000000000000000000000000000000000000000000 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
之后:
000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
gas 使用量:6
MCOPY 0 0 32
- 将 32 字节从偏移量 0 复制到偏移量 0。
之前:
0101010101010101010101010101010101010101010101010101010101010101
之后:
0101010101010101010101010101010101010101010101010101010101010101
gas 使用量:6
MCOPY 0 1 8
- 将 8 字节从偏移量 1 复制到偏移量 0(重叠)。
之前(第 8 个字节处有空格):
0001020304050607 080000000000000000000000000000000000000000000000
之后:
0102030405060708 080000000000000000000000000000000000000000000000
gas 使用量:6
MCOPY 1 0 8
- 将 8 字节从偏移量 0 复制到偏移量 1(重叠)。
之前(第 8 个字节处有空格):
0001020304050607 080000000000000000000000000000000000000000000000
之后:
0000010203040506 070000000000000000000000000000000000000000000000
gas 使用量:6
完整测试套件
可以在执行规范测试中找到完整的测试套件:MCOPY 套件。
安全注意事项
客户端应注意,他们的实现不使用中间缓冲区(例如,请参阅 C stdlib 的 memmove
函数未使用中间缓冲区),因为这是一种潜在的拒绝服务 (DoS) 向量。大多数用于移动字节的语言内置函数/标准库函数在此处具有正确的性能特征。
除此之外,拒绝服务 (DoS) 和内存耗尽攻击的分析与其他接触内存的操作码相同,因为内存扩展遵循相同的定价规则。
版权
通过 CC0 放弃版权及相关权利。
Citation
Please cite this document as:
Alex Beregszaszi (@axic), Paul Dworzanski (@poemm), Jared Wasinger (@jwasinger), Casey Detrio (@cdetrio), Pawel Bylica (@chfast), Charles Cooper (@charles-cooper), "EIP-5656: MCOPY - 内存复制指令," Ethereum Improvement Proposals, no. 5656, February 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5656.