Solidity 汇编的 gas 优化 Keccak256

  • Dacian
  • 更新于 1天前
  • 阅读 150

介绍了如 Solidity 智能合约中使用内联汇编语言(Inline Assembly)实现keccak256哈希函数的优化方法.

Solidity 智能合约通常使用 keccak256 对多个输入参数进行哈希;调用此函数的标准方式如下所示:

    function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {
        result = keccak256(abi.encode(a,b,c));
    }

然而,通过以下汇编代码,可以将计算哈希的 gas 成本降低约 42%:

    function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {
        assembly {
            let mPtr := mload(0x40)
            mstore(mPtr, a)
            mstore(add(mPtr, 0x20), b)
            mstore(add(mPtr, 0x40), c)

            result := keccak256(mPtr, 0x60)
        }
    }

让我们来看看这种节省 gas 成本的原因和原理!

前置知识

为了理解下一部分内容,你需要完成 Updraft 的 Assembly & Formal Verification course 的第一部分,或者具有以下等效知识:

Foundry 测试代码

为了检查两个函数的执行,我们将使用以下独立的 Foundry 测试合约:

    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.25;

    import "forge-std/Test.sol";

    // 为每个实现创建单独的合约,通过测试合约中的接口访问实现
    // 防止优化器过于“智能”,帮助更好地近似真实世界的执行
    interface IGasImpl {
        function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result);
    }

    contract GasImplNormal is IGasImpl {
        function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {
            result = keccak256(abi.encode(a,b,c));
        }
    }

    contract GasImplAssembly is IGasImpl {
        function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {
            assembly {
                let mPtr := mload(0x40)
                mstore(mPtr, a)
                mstore(add(mPtr, 0x20), b)
                mstore(add(mPtr, 0x40), c)

                result := keccak256(mPtr, 0x60)
            }
        }
    }

    // 实际测试合约
    contract GasDebugTest is Test {
        IGasImpl gasImplNormal   = new GasImplNormal();
        IGasImpl gasImplAssembly = new GasImplAssembly();
        uint256 a = 1;
        uint256 b = 2;
        uint256 c = 3;

        // forge test --match-contract GasDebugTest --debug test_GasImplNormal
        function test_GasImplNormal() external {
            bytes32 result = gasImplNormal.getKeccak256(a,b,c);
            assertEq(result, 0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c);
        }

        // forge test --match-contract GasDebugTest --debug test_GasImplAssembly
        function test_GasImplAssembly() external {
            bytes32 result = gasImplAssembly.getKeccak256(a,b,c);
            assertEq(result, 0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c);
        }
    }

执行跟踪(trace) - 汇编版本

接下来,我们将使用 Foundry 的调试器逐步检查汇编版本,执行以下命令:forge test --match-contract GasDebugTest --debug test_GasImplAssembly

我们关注的是 GasImplAssembly 合约内部的执行:

  • 从第一个 PUSH1(0x40) 开始,直到调用 keccak256 并将计算得到的哈希放到堆栈上

上述执行从 PC 0x39 (57) 开始,到 0x4f (79) 结束,消耗了 341-224 = 117 gas。一些有用的缩写包括:

  • 自由内存指针地址 (FMPA)

  • 下一个自由内存地址的起始值 (SNFMA)

  • 下一个自由内存地址 (NFMA)

让我们逐步检查执行,了解汇编版本的工作原理:

    // 将自由内存指针地址 (FMPA) 推入堆栈
    PUSH1(0x40) [Stack : 0x40, 0x03, 0x02, 0x01, 0x52, 0x05536b19]    
                [Memory: 0x40 = 0x80                             ]

    // 复制 FMPA
    DUP1        [Stack : 0x40, 0x40, 0x03, 0x02, 0x01, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80                                   ]

    // 通过读取 FMPA 加载下一个自由内存地址 (SNFMA)
    MLOAD       [Stack : 0x80, 0x40, 0x03, 0x02, 0x01, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80                                   ]

    // 交换堆栈中第 5 和第 1 项
    // 注意:这是一种常见模式,用于在内存中存储输入参数
    SWAP4       [Stack : 0x01, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80                                   ]

    // 复制 SNFMA
    DUP5        [Stack : 0x80, 0x01, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80                                         ]

    // 将值 `a` 存储到 SNFMA 指向的内存中
    MSTORE      [Stack : 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80, 0x80 = 0x01                ]

    // 将 0x20 推入堆栈 (第一个 `add` 调用的第二个参数)
    // 此值是计算下一个自由内存地址 (NFMA) 的偏移量
    PUSH1(0x20) [Stack : 0x20, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]

    // 复制 SNFMA
    DUP5        [Stack : 0x80, 0x20, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80, 0x80 = 0x01                            ]

    // 通过 SNFMA + 偏移量 计算 NFMA (0x80 + 0x20)
    ADD         [Stack : 0xa0, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]

    // 交换堆栈中第 4 和第 1 项
    SWAP3       [Stack : 0x02, 0x40, 0x03, 0xa0, 0x80, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]

    // 交换堆栈中第 2 和第 1 项
    SWAP1       [Stack : 0x40, 0x02, 0x03, 0xa0, 0x80, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]

    // 交换堆栈中第 4 和第 1 项
    SWAP3       [Stack : 0xa0, 0x02, 0x03, 0x40, 0x80, 0x52, 0x05536b19]
                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]

    // 将值 `b` 存储到 NFMA 指向的内存中
    MSTORE      [Stack : 0x03, 0x40, 0x80, 0x52, 0x05536b19   ]
                [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02]

// 通过 SNFMA + 偏移量 (0x80 + 0x40) 计算 NFMA
ADD         [Stack : 0xc0, 0x03, 0x80, 0x52, 0x05536b19   ]
            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02]

// 将 `c` 的值存储到 NFMA 地址的内存中
MSTORE      [Stack : 0x80, 0x52, 0x05536b19                            ]
            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]

// 注意:所有输入参数现在都存储在内存中

// 将 `size` 参数推送到栈中以供调用 keccak
PUSH1(0x60) [Stack : 0x60, 0x80, 0x52, 0x05536b19                      ]
            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]

// 交换栈中第二和第一元素
SWAP1       [Stack : 0x80, 0x60, 0x52, 0x05536b19                      ]
            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]

// 调用 keccak256(offset, size)
KECCAK256   [Stack : result, 0x52, 0x05536b19                          ]
            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]

汇编版本:

  • 将第一个输入参数 a 存储在下一个可用内存地址 (SNFMA)

  • 计算了 2 个额外的下一个可用内存地址 (NFMA),用于存储输入参数 b, c

  • 一旦完成,仅需要再执行 3 个操作码;2 个用于准备 偏移量, 大小 输入参数,最后一个用于调用 keccak256

  • 共执行了 20 个操作码,包括 1 个 MLOAD 和 3 个 MSTORE

执行跟踪(trace) - Solidity 版本

在检查了汇编版本后,我们现在将转向使用 Foundry 的调试器的 Solidity 版本,通过执行以下命令:forge test --match-contract GasDebugTest --debug test_GasImplNormal

我们关心的是 GasImplNormal 合约中的执行:

  • 从第一个 PUSH1(0x40) 开始在将 calldata 加载到栈中并调用 JUMPDEST

  • keccak256 被调用并计算出的哈希放置在栈上为止

上述执行从 PC 0x39 (57) 开始,直到 0x6c (108),使用了 428-224 = 204 gas,比汇编版本多 74%!

// 将自由内存指针地址 (FMPA) 推送到栈上
PUSH1(0x40) [Stack : 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                             ]

// 复制 FMPA
DUP1        [Stack : 0x40, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                                   ]

// 通过读取 FMPA 加载起始下一个可用内存地址 (SNFMA)
MLOAD       [Stack : 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                                   ]

// 将 0x20 推送到栈上
// 这是计算下一个可用内存地址 (NFMA) 的偏移量
PUSH1(0x20) [Stack : 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                                         ]

// 复制偏移量
DUP1        [Stack : 0x20, 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                                               ]

// 复制 SNFMA
DUP3        [Stack : 0x80, 0x20, 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                                                     ]

// 通过 SNFMA + 偏移量 (0x80 + 0x20) 计算 NFMA
ADD         [Stack : 0xa0, 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                                               ]

// 交换栈中第七和第一个元素
// 注意:遵循一个常见的模式以在内存中存储输入参数
SWAP6       [Stack : 0x01, 0x20, 0x80, 0x40, 0x03, 0x02, 0xa0, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                                               ]

// 交换栈中第二和第一个元素
SWAP1       [Stack : 0x20, 0x01, 0x80, 0x40, 0x03, 0x02, 0xa0, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                                               ]

// 交换栈中第七和第一个元素
SWAP6       [Stack : 0xa0, 0x01, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80                                               ]

// 将 `a` 的值存储到 NFMA 地址的内存中
MSTORE      [Stack : 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01                      ]

// 注意:正常版本未将第一个输入存储在 SNFMA,因此需要多计算一个 NFMA 
// 以将所有输入存储到内存中,相比于组合版本

// 复制 SNFMA
DUP1        [Stack : 0x80, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]

// 复制 FMPA
DUP3        [Stack : 0x40, 0x80, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01                                  ]

// 通过 FMPA + 偏移量 (0x40 + 0x80) 计算 NFMA
ADD         [Stack : 0xc0, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]

// 交换栈中第五和第一个元素
SWAP4       [Stack : 0x02, 0x80, 0x40, 0x03, 0xc0, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]

// 交换栈中第二和第一个元素
SWAP1       [Stack : 0x80, 0x02, 0x40, 0x03, 0xc0, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]

// 交换栈中第五和第一个元素
SWAP4       [Stack : 0xc0, 0x02, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]

// 将 `b` 的值存储到 NFMA 地址的内存中
MSTORE      [Stack : 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02   ]

// 另一个偏移量用于计算 NFMA
PUSH1(0x60) [Stack : 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02         ]

// 复制偏移量
DUP1        [Stack : 0x60, 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]

// 复制 SNFMA
DUP5        [Stack : 0x80, 0x60, 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02                     ]

// 通过 SNFMA + 偏移量 (0x80 + 0x60) 计算 NFMA
ADD         [Stack : 0xe0, 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]

// 交换栈中第四和第一个元素
SWAP3       [Stack : 0x03, 0x60, 0x40, 0xe0, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]

// 交换栈中第二和第一个元素
SWAP1       [Stack : 0x60, 0x03, 0x40, 0xe0, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]

// 交换第 4 个和第 1 个栈元素
SWAP3       [Stack : 0xe0, 0x03, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]

// 将 `c` 的值存储到 NFMA 的内存中
MSTORE      [Stack : 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19          ]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 注意:所有输入参数现在都存储在内存中

// 复制 FMPA
DUP1        [Stack : 0x40, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19    ]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 通过读取 FMPA 来加载当前下一个可用内存地址 (SNFMA)
MLOAD       [Stack : 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19    ]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 复制 SNFMA
// 注意:后续操作码与 `abi.encode` 相关
DUP1        [Stack : 0x80, 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]

// 复制 SNFMA
DUP5        [Stack : 0x80, 0x80, 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03        ]

// 将第 2 个元素从第 1 个中减去
SUB         [Stack : 0x00, 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]

// 交换第 2 个和第 1 个栈元素
SWAP1       [Stack : 0x80, 0x00, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]

// 交换第 4 个和第 1 个栈元素
SWAP3       [Stack : 0x60, 0x00, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]

// 计算 `size` 参数,用于后续 keccak256 调用
ADD         [Stack : 0x60, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19    ]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 复制 SNFMA
DUP3        [Stack : 0x80, 0x60, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19]
            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]

// 将 `size` 参数 (0x60) 存储在 SNFMA 中
// 后续将用于 keccak256 调用
MSTORE      [Stack : 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19                       ]
            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 将 SNFMA 压入栈中
PUSH1(0x80) [Stack : 0x80, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19                 ]
            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 交换第 2 个和第 1 个栈元素
SWAP1       [Stack : 0x40, 0x80, 0x80, 0x80, 0x20, 0x6f, 0x05536b19                 ]
            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 交换第 4 个和第 1 个栈元素
SWAP3       [Stack : 0x80, 0x80, 0x80, 0x40, 0x20, 0x6f, 0x05536b19                 ]
            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 计算 NFMA
ADD         [Stack : 0x100, 0x80, 0x40, 0x20, 0x6f, 0x05536b19                      ]
            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 交换第 2 个和第 1 个栈元素
SWAP1       [Stack : 0x80, 0x100, 0x40, 0x20, 0x6f, 0x05536b19                      ]
            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 交换第 3 个和第 1 个栈元素
SWAP2       [Stack : 0x40, 0x100, 0x80, 0x20, 0x6f, 0x05536b19                      ]
            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 用下一个 NFMA 覆盖 FMPA 的值
MSTORE      [Stack : 0x80, 0x20, 0x6f, 0x05536b19                                    ]
            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 注意:普通版本必须计算第二个额外的 NFMA
// 并在 FMPA 更新内存;优化版本没有这样做

// 复制 SNFMA
DUP1        [Stack : 0x80, 0x80, 0x20, 0x6f, 0x05536b19                              ]
            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 将存储在 SNFMA 中的值放入栈中
// 这将是用于 keccak256 的 `size` 参数
// 调用之前计算的
MLOAD       [Stack : 0x60, 0x80, 0x20, 0x6f, 0x05536b19                              ]
            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 交换第 3 个和第 1 个栈元素
SWAP2       [Stack : 0x20, 0x80, 0x60, 0x6f, 0x05536b19                              ]
            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 计算第一个参数的内存地址;用作
// keccak256 的 `offset`
ADD         [Stack : 0xa0, 0x60, 0x6f, 0x05536b19                                    ]
            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 调用 keccak256(offset, size)
KECCAK256   [result, 0x6f, 0x05536b19                                                ]
            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

Gas 使用比较

与汇编版本相比,Solidity 版本:

  • 未在起始下一个可用内存地址 (SNFMA) 中存储第一个输入参数 a

  • 相反,它计算了 3 个额外的下一个可用内存地址 (NFMA),并用于存储输入参数 a, b, c

  • 一旦完成,与汇编版本的 3 个操作码相比,增加了 22 个操作码

  • 计算了用于 keccak256offset 参数,而汇编版本中是硬编码的

  • 更新了自由内存指针地址 (FMPA),而汇编版本不需要这样做,即使它没有使用更新的地址 (0x100)

  • 总共执行了 48 个操作码,而汇编版本为 20 个操作码,包括 3 个 MLOAD 和 5 个 MSTORE

  • 使用了 204 gas,而汇编版本为 117,导致 gas 使用增加了 74% (204-117=87, (87/117)*100 = 74)

  • 因此,汇编版本相比于 Solidity 版本节省了 42% 的 gas (204-117=87, (87/204)*100 = 42)

用 --via-ir 编译有帮助吗?

使用 --via-ir 编译为相关代码提供了适度的 gas 改进:

  • 汇编版本使用 108 gas,从 117 gas 降低

  • Solidity 版本使用 195 gas,从 204 gas 降低

相关执行跟踪如下。

执行跟踪(trace) - 汇编 --via-ir 版本

执行此命令:forge test --match-contract GasDebugTest --debug test_GasImplAssembly --via-ir

我们关心 GasImplAssembly 合约内部的执行:

  • 从第一个输入参数 a 的第一个 CALLDATALOAD 开始

  • 直到调用 keccak256 并将计算出的哈希放入堆栈

上述执行从 PC 0x32 (50) 开始到 0x46 (70),使用了 213-105 = 108 gas:

    // 从 calldata 将 `a` 的值推送到堆栈
    CALLDATALOAD [Stack : 0x01]    
                 [Memory:     ]

    // 将下一个自由内存地址(SNFMA)推送到堆栈
    PUSH1(0x80)  [Stack : 0x80, 0x01]
                 [Memory:           ]

    // 将 `a` 的值存储到 SNFMA 的内存中
    MSTORE       [Stack :            ]
                 [Memory: 0x80 = 0x01]

    // 将偏移量推送到堆栈以读取下一个输入变量
    PUSH1(0x24)  [Stack : 0x24       ]
                 [Memory: 0x80 = 0x01]

    // 从 calldata 将 `b` 的值推送到堆栈
    CALLDATALOAD [Stack : 0x02       ]
                 [Memory: 0x80 = 0x01]

    // 将下一个自由内存地址(NFMA)推送到堆栈 
    PUSH1(0xa0)  [Stack : 0xa0, 0x02 ]
                 [Memory: 0x80 = 0x01]

    // 注意:--via-ir 能够预计算自由内存地址
    // 这样它不必在运行时计算它们,而可以直接将它们推送到堆栈

    // 将 `b` 的值存储到 NFMA 的内存中
    MSTORE       [Stack :                         ]
                 [Memory: 0x80 = 0x01, 0xa0 = 0x02]

    // 将偏移量推送到堆栈以读取下一个输入变量
    PUSH1(0x44)  [Stack : 0x44                    ]
                 [Memory: 0x80 = 0x01, 0xa0 = 0x02]

    // 从 calldata 将 `c` 的值推送到堆栈
    CALLDATALOAD [Stack : 0x03                    ]
                 [Memory: 0x80 = 0x01, 0xa0 = 0x02]

    // 将下一个自由内存地址(NFMA)推送到堆栈 
    PUSH1(0xc0)  [Stack : 0xc0, 0x03              ]
                 [Memory: 0x80 = 0x01, 0xa0 = 0x02]

    // 将 `c` 的值存储到 NFMA 的内存中
    MSTORE       [Stack :                                      ]
                 [Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]

    // 注意:所有输入参数现在都存储在内存中

    // 推送 `size` 参数以便调用 keccak 到堆栈
    PUSH1(0x60)  [Stack : 0x60                                 ]
                 [Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]

    // 推送 `offset` 参数以便调用 keccak 到堆栈
    PUSH1(0x80)  [Stack : 0x80, 0x60                           ]
                 [Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]

    // 调用 keccak256(offset, size)
    KECCAK256   [Stack : result                               ]
                [Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]

执行跟踪 - Solidity --via-ir 版本

执行此命令: forge test --match-contract GasDebugTest --debug test_GasImplNormal --via-ir

我们关注的是 GasImplNormal 合约内的执行:

  • 从第一个输入参数 a 的第一个 CALLDATALOAD 开始

  • 直到调用 keccak256 并将计算出的哈希放入堆栈

上述执行从 PC 0x3a (58) 开始到 0x72 (114),使用了 318-123 = 195 gas:

    // 从 calldata 将 `a` 的值推送到堆栈
    CALLDATALOAD [Stack : 0x01, 0xa0, 0x80, 0x00]    
                 [Memory:                       ]

    // 将下一个自由内存地址(NFMA)重复推送到堆栈
    DUP2         [Stack : 0x0a, 0x01, 0xa0, 0x80, 0x00]    
                 [Memory:                             ]

    // 将 `a` 的值存储到 NFMA 的内存中
    MSTORE       [Stack : 0xa0, 0x80, 0x00]    
                 [Memory: 0xa0 = 0x01     ]

    // 将偏移量推送到堆栈以读取下一个输入变量
    PUSH1(0x24)  [Stack : 0x24, 0xa0, 0x80, 0x00]
                 [Memory: 0xa0 = 0x01           ]

    // 从 calldata 将 `b` 的值推送到堆栈
    CALLDATALOAD [Stack : 0x02, 0xa0, 0x80, 0x00]
                 [Memory: 0xa0 = 0x01           ]

    // 将自由内存指针地址(FMPA)推送到堆栈 
    PUSH1(0x40)  [Stack : 0x40, 0x02, 0xa0, 0x80, 0x00]
                 [Memory: 0xa0 = 0x01                 ]

    // 重复 0x80
    DUP4         [Stack : 0x80, 0x40, 0x02, 0xa0, 0x80, 0x00]
                 [Memory: 0xa0 = 0x01                       ]

    // 计算下一个自由内存地址(NFMA)
    ADD          [Stack : 0xc0, 0x02, 0xa0, 0x80, 0x00]
                 [Memory: 0xa0 = 0x01                 ]

    // 将 `b` 的值存储到 NFMA 的内存中
    MSTORE       [Stack : 0xa0, 0x80, 0x00        ]    
                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02] 

    // 将偏移量推送到堆栈以读取下一个输入变量
    PUSH1(0x44)  [Stack : 0x44, 0xa0, 0x80, 0x00  ]
                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02]

    // 从 calldata 将 `c` 的值推送到堆栈
    CALLDATALOAD [Stack : 0x03, 0xa0, 0x80, 0x00  ]
                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02]

    // 推送偏移量以计算 NFMA
    PUSH1(0x60)  [Stack : 0x60, 0x03, 0xa0, 0x80, 0x00]
                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02    ]

    // 重复 0x80
    DUP4         [Stack : 0x80, 0x60, 0x03, 0xa0, 0x80, 0x00]
                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02          ]

    // 计算 NFMA
    ADD          [Stack : 0xe0, 0x03, 0xa0, 0x80, 0x00]
                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02    ]

    // 将 `c` 的值存储到 NFMA 的内存中
    MSTORE       [Stack : 0xa0, 0x80, 0x00                     ]
                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

    // 注意:所有输入参数现在都存储在内存中

    // 推送 0x60,将保存到内存,并将在后面
    // 用作 keccak256 调用的 `size` 参数
    PUSH1(0x60)  [Stack : 0x60, 0xa0, 0x80, 0x00               ]
                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

    // 注意:后续与 `abi.encode` 相关的操作码

    // 复制内存地址以保存先前推送的 `size` 参数
    DUP3         [Stack : 0x80, 0x60, 0xa0, 0x80, 0x00         ]
                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

    // 将 `size` 参数保存到内存中
    MSTORE       [Stack : 0xa0, 0x80, 0x00                                  ]
                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

    // 用于计算 NFMA
    PUSH1(0x80)  [Stack : 0x80, 0xa0, 0x80, 0x00                            ]
                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

    // 用于计算 NFMA
    DUP3         [Stack : 0x80, 0x80, 0xa0, 0x80, 0x00                      ]
                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

    // 计算 NFMA;该值用于 LT 和 GT 比较
    // 然后稍后保存到 FMPA
    ADD          [Stack : 0x100, 0xa0, 0x80, 0x00                           ]
                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

    // 交换第 3 和第 1 个堆栈元素
    SWAP2        [Stack : 0x80, 0xa0, 0x100, 0x00                           ]
                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

    // 用于 LT 比较
    DUP1         [Stack : 0x80, 0x80, 0xa0, 0x100, 0x00                     ]
                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 用于 LT 比较
DUP4         [Stack : 0x100, 0x80, 0x80, 0xa0, 0x100, 0x00              ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 0x100 < 0x80 ? false
LT           [Stack : 0x00, 0x80, 0xa0, 0x100, 0x00                     ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 用于 GT 比较
PUSH8(0xffffffffffffffff)
             [Stack : 0xffffffffffffffff, 0x00, 0x80, 0xa0, 0x100, 0x00 ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 复制
DUP5         [Stack : 0x100, 0xffffffffffffffff, 0x00, 0x80, 0xa0, 0x100, 0x00]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03      ]

// 0x100 > 0xffffffffffffffff ? false
GT           [Stack : 0x00, 0x00, 0x80, 0xa0, 0x100, 0x00               ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 0x00 或 0x00 = 0x00
OR           [Stack : 0x00, 0x80, 0xa0, 0x100, 0x00                     ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 不确定为什么这里会被推送
PUSH1(0x76)  [Stack : 0x76, 0x00, 0x80, 0xa0, 0x100, 0x00               ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 跳转? false
JUMPI        [Stack : 0x80, 0xa0, 0x100, 0x00                           ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 不确定为什么这里会被推送
PUSH1(0x20)  [Stack : 0x20, 0x80, 0xa0, 0x100, 0x00                     ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 交换第 5 个和第 1 个栈元素
SWAP4        [Stack : 0x00, 0x80, 0xa0, 0x100, 0x20                     ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 移除顶部元素 0x00
POP          [Stack : 0x80, 0xa0, 0x100, 0x20                           ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 复制 
DUP3         [Stack : 0x100, 0x80, 0xa0, 0x100, 0x20                    ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 准备用先前计算的 NFMA 覆盖 FMPA
PUSH1(0x40)  [Stack : 0x40, 0x100, 0x80, 0xa0, 0x100, 0x20              ]
             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 用下一个 NFMA 覆盖 FMPA 的值
// 但 FMPA 并没有被使用?
MSTORE       [Stack : 0x80, 0xa0, 0x100, 0x20                                         ]
             [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 加载 keccak256 调用的 `size` 参数
MLOAD        [Stack : 0x60, 0xa0, 0x100, 0x20                                         ]
             [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 交换第 2 个和第 1 个栈元素
SWAP1        [Stack : 0xa0, 0x60, 0x100, 0x20                                         ]
             [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

// 调用 keccak256(offset, size)
KECCAK256    [Stack : result, 0x100, 0x20                                             ]
             [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]

使用 Halmos 进行形式验证

可以将以下函数添加到现有的 GasDebugTest 合约中,以便正式验证使用 Halmos 证明汇编和 Solidity 版本生成相同的输出:

// halmos --match-contract GasDebugTest
function check_GasImplEquivalent(uint256 a1, uint256 b1, uint256 c1) external {
    bytes32 resultNormal   = gasImplNormal.getKeccak256(a1,b1,c1);
    bytes32 resultAssembly = gasImplAssembly.getKeccak256(a1,b1,c1);

    assertEq(resultNormal, resultAssembly);
}

执行形式验证使用:halmos --match-contract GasDebugTest

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Dacian
Dacian
in your storage