Alert Source Discuss
Standards Track: Core

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,其中它被认为是一个重要的开销。

规范

指令 MCOPY0x5E 处引入。

输入栈

top - 0 dst
top - 1 src
top - 2 length

此顺序与其他复制指令(即 CALLDATACOPYRETURNDATACOPY)相匹配。

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 + lengthdst + 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.