{"content":{"title":"Solidity 汇编的 gas 优化 Keccak256","body":">- 原文链接：[dacian.me/solidity-ass...](https://dacian.me/solidity-assembly-gas-optimized-keccak256)\r\n>- 译者：[AI翻译官](https://learnblockchain.cn/people/19584)，校对：[翻译小组](https://learnblockchain.cn/people/412)\r\n>- 本文链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/10520)\r\n    \r\nSolidity 智能合约通常使用 `keccak256` 对多个输入参数进行哈希；调用此函数的标准方式如下所示：\r\n\r\n```\r\n    function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {\r\n        result = keccak256(abi.encode(a,b,c));\r\n    }\r\n```\r\n    \r\n\r\n然而，通过以下汇编代码，可以将计算哈希的 gas 成本降低约 42%：\r\n```\r\n    function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {\r\n        assembly {\r\n            let mPtr := mload(0x40)\r\n            mstore(mPtr, a)\r\n            mstore(add(mPtr, 0x20), b)\r\n            mstore(add(mPtr, 0x40), c)\r\n    \r\n            result := keccak256(mPtr, 0x60)\r\n        }\r\n    }\r\n```\r\n    \r\n\r\n让我们来看看这种节省 gas 成本的原因和原理！\r\n\r\n##  前置知识\r\n\r\n为了理解下一部分内容，你需要完成 Updraft 的 [Assembly & Formal Verification course](https://updraft.cyfrin.io/courses/formal-verification) 的第一部分，或者具有以下等效知识：\r\n\r\n*   [EVM Opcodes](https://www.evm.codes/)\r\n    \r\n*   [EVM Stack Machine](https://faizannehal.medium.com/understanding-the-stack-based-architecture-of-evm-af45dc9819f2)\r\n    \r\n*   Solidity 的 [Free Memory Pointer](https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html)\r\n    \r\n*   [Foundry's Debugger](https://book.getfoundry.sh/forge/debugger)\r\n    \r\n\r\n##  Foundry 测试代码\r\n\r\n为了检查两个函数的执行，我们将使用以下独立的 Foundry 测试合约：\r\n```\r\n    // SPDX-License-Identifier: MIT\r\n    pragma solidity 0.8.25;\r\n    \r\n    import \"forge-std/Test.sol\";\r\n    \r\n    // 为每个实现创建单独的合约，通过测试合约中的接口访问实现\r\n    // 防止优化器过于“智能”，帮助更好地近似真实世界的执行\r\n    interface IGasImpl {\r\n        function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result);\r\n    }\r\n    \r\n    contract GasImplNormal is IGasImpl {\r\n        function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {\r\n            result = keccak256(abi.encode(a,b,c));\r\n        }\r\n    }\r\n    \r\n    contract GasImplAssembly is IGasImpl {\r\n        function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {\r\n            assembly {\r\n                let mPtr := mload(0x40)\r\n                mstore(mPtr, a)\r\n                mstore(add(mPtr, 0x20), b)\r\n                mstore(add(mPtr, 0x40), c)\r\n    \r\n                result := keccak256(mPtr, 0x60)\r\n            }\r\n        }\r\n    }\r\n    \r\n    // 实际测试合约\r\n    contract GasDebugTest is Test {\r\n        IGasImpl gasImplNormal   = new GasImplNormal();\r\n        IGasImpl gasImplAssembly = new GasImplAssembly();\r\n        uint256 a = 1;\r\n        uint256 b = 2;\r\n        uint256 c = 3;\r\n    \r\n        // forge test --match-contract GasDebugTest --debug test_GasImplNormal\r\n        function test_GasImplNormal() external {\r\n            bytes32 result = gasImplNormal.getKeccak256(a,b,c);\r\n            assertEq(result, 0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c);\r\n        }\r\n    \r\n        // forge test --match-contract GasDebugTest --debug test_GasImplAssembly\r\n        function test_GasImplAssembly() external {\r\n            bytes32 result = gasImplAssembly.getKeccak256(a,b,c);\r\n            assertEq(result, 0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c);\r\n        }\r\n    }\r\n```    \r\n\r\n##  执行跟踪(trace) - 汇编版本\r\n\r\n接下来，我们将使用 Foundry 的调试器逐步检查汇编版本，执行以下命令：`forge test --match-contract GasDebugTest --debug test_GasImplAssembly`\r\n\r\n我们关注的是 `GasImplAssembly` 合约内部的执行：\r\n\r\n*   从第一个 `PUSH1(0x40)` 开始，直到调用 `keccak256` 并将计算得到的哈希放到堆栈上\r\n\r\n上述执行从 PC 0x39 (57) 开始，到 0x4f (79) 结束，消耗了 341-224 = 117 gas。一些有用的缩写包括：\r\n\r\n*   自由内存指针地址 (FMPA)\r\n    \r\n*   下一个自由内存地址的起始值 (SNFMA)\r\n    \r\n*   下一个自由内存地址 (NFMA)\r\n    \r\n\r\n让我们逐步检查执行，了解汇编版本的工作原理：\r\n```\r\n    // 将自由内存指针地址 (FMPA) 推入堆栈\r\n    PUSH1(0x40) [Stack : 0x40, 0x03, 0x02, 0x01, 0x52, 0x05536b19]    \r\n                [Memory: 0x40 = 0x80                             ]\r\n    \r\n    // 复制 FMPA\r\n    DUP1        [Stack : 0x40, 0x40, 0x03, 0x02, 0x01, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80                                   ]\r\n    \r\n    // 通过读取 FMPA 加载下一个自由内存地址 (SNFMA)\r\n    MLOAD       [Stack : 0x80, 0x40, 0x03, 0x02, 0x01, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80                                   ]\r\n    \r\n    // 交换堆栈中第 5 和第 1 项\r\n    // 注意：这是一种常见模式，用于在内存中存储输入参数\r\n    SWAP4       [Stack : 0x01, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80                                   ]\r\n    \r\n    // 复制 SNFMA\r\n    DUP5        [Stack : 0x80, 0x01, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80                                         ]\r\n    \r\n    // 将值 `a` 存储到 SNFMA 指向的内存中\r\n    MSTORE      [Stack : 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80, 0x80 = 0x01                ]\r\n    \r\n    // 将 0x20 推入堆栈 (第一个 `add` 调用的第二个参数)\r\n    // 此值是计算下一个自由内存地址 (NFMA) 的偏移量\r\n    PUSH1(0x20) [Stack : 0x20, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]\r\n    \r\n    // 复制 SNFMA\r\n    DUP5        [Stack : 0x80, 0x20, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80, 0x80 = 0x01                            ]\r\n    \r\n    // 通过 SNFMA + 偏移量 计算 NFMA (0x80 + 0x20)\r\n    ADD         [Stack : 0xa0, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]\r\n    \r\n    // 交换堆栈中第 4 和第 1 项\r\n    SWAP3       [Stack : 0x02, 0x40, 0x03, 0xa0, 0x80, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]\r\n    \r\n    // 交换堆栈中第 2 和第 1 项\r\n    SWAP1       [Stack : 0x40, 0x02, 0x03, 0xa0, 0x80, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]\r\n    \r\n    // 交换堆栈中第 4 和第 1 项\r\n    SWAP3       [Stack : 0xa0, 0x02, 0x03, 0x40, 0x80, 0x52, 0x05536b19]\r\n                [Memory: 0x40 = 0x80, 0x80 = 0x01                      ]\r\n    \r\n    // 将值 `b` 存储到 NFMA 指向的内存中\r\n    MSTORE      [Stack : 0x03, 0x40, 0x80, 0x52, 0x05536b19   ]\r\n                [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02]\r\n\r\n\r\n// 通过 SNFMA + 偏移量 (0x80 + 0x40) 计算 NFMA\r\nADD         [Stack : 0xc0, 0x03, 0x80, 0x52, 0x05536b19   ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02]\r\n\r\n// 将 `c` 的值存储到 NFMA 地址的内存中\r\nMSTORE      [Stack : 0x80, 0x52, 0x05536b19                            ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]\r\n\r\n// 注意：所有输入参数现在都存储在内存中\r\n\r\n// 将 `size` 参数推送到栈中以供调用 keccak\r\nPUSH1(0x60) [Stack : 0x60, 0x80, 0x52, 0x05536b19                      ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]\r\n\r\n// 交换栈中第二和第一元素\r\nSWAP1       [Stack : 0x80, 0x60, 0x52, 0x05536b19                      ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]\r\n\r\n// 调用 keccak256(offset, size)\r\nKECCAK256   [Stack : result, 0x52, 0x05536b19                          ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]\r\n```\r\n\r\n汇编版本：\r\n\r\n*   将第一个输入参数 `a` 存储在下一个可用内存地址 (SNFMA)\r\n    \r\n*   计算了 2 个额外的下一个可用内存地址 (NFMA)，用于存储输入参数 `b, c`\r\n    \r\n*   一旦完成，仅需要再执行 3 个操作码；2 个用于准备 `偏移量, 大小` 输入参数，最后一个用于调用 `keccak256`\r\n    \r\n*   共执行了 20 个操作码，包括 1 个 `MLOAD` 和 3 个 `MSTORE`\r\n\r\n\r\n##   执行跟踪(trace) - Solidity 版本\r\n\r\n在检查了汇编版本后，我们现在将转向使用 Foundry 的调试器的 Solidity 版本，通过执行以下命令：`forge test --match-contract GasDebugTest --debug test_GasImplNormal`\r\n\r\n我们关心的是 `GasImplNormal` 合约中的执行：\r\n\r\n*   从第一个 `PUSH1(0x40)` 开始在将 calldata 加载到栈中并调用 `JUMPDEST` 后\r\n    \r\n*   到 `keccak256` 被调用并计算出的哈希放置在栈上为止\r\n\r\n\r\n上述执行从 PC 0x39 (57) 开始，直到 0x6c (108)，使用了 428-224 = 204 gas，比汇编版本多 74%！\r\n\r\n```\r\n// 将自由内存指针地址 (FMPA) 推送到栈上\r\nPUSH1(0x40) [Stack : 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                             ]\r\n\r\n// 复制 FMPA\r\nDUP1        [Stack : 0x40, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                                   ]\r\n\r\n// 通过读取 FMPA 加载起始下一个可用内存地址 (SNFMA)\r\nMLOAD       [Stack : 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                                   ]\r\n\r\n// 将 0x20 推送到栈上\r\n// 这是计算下一个可用内存地址 (NFMA) 的偏移量\r\nPUSH1(0x20) [Stack : 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                                         ]\r\n\r\n// 复制偏移量\r\nDUP1        [Stack : 0x20, 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                                               ]\r\n\r\n// 复制 SNFMA\r\nDUP3        [Stack : 0x80, 0x20, 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                                                     ]\r\n\r\n// 通过 SNFMA + 偏移量 (0x80 + 0x20) 计算 NFMA\r\nADD         [Stack : 0xa0, 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                                               ]\r\n\r\n// 交换栈中第七和第一个元素\r\n// 注意：遵循一个常见的模式以在内存中存储输入参数\r\nSWAP6       [Stack : 0x01, 0x20, 0x80, 0x40, 0x03, 0x02, 0xa0, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                                               ]\r\n\r\n// 交换栈中第二和第一个元素\r\nSWAP1       [Stack : 0x20, 0x01, 0x80, 0x40, 0x03, 0x02, 0xa0, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                                               ]\r\n\r\n// 交换栈中第七和第一个元素\r\nSWAP6       [Stack : 0xa0, 0x01, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80                                               ]\r\n\r\n// 将 `a` 的值存储到 NFMA 地址的内存中\r\nMSTORE      [Stack : 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01                      ]\r\n\r\n// 注意：正常版本未将第一个输入存储在 SNFMA，因此需要多计算一个 NFMA \r\n// 以将所有输入存储到内存中，相比于组合版本\r\n\r\n// 复制 SNFMA\r\nDUP1        [Stack : 0x80, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]\r\n\r\n// 复制 FMPA\r\nDUP3        [Stack : 0x40, 0x80, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01                                  ]\r\n\r\n// 通过 FMPA + 偏移量 (0x40 + 0x80) 计算 NFMA\r\nADD         [Stack : 0xc0, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]\r\n\r\n// 交换栈中第五和第一个元素\r\nSWAP4       [Stack : 0x02, 0x80, 0x40, 0x03, 0xc0, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]\r\n\r\n// 交换栈中第二和第一个元素\r\nSWAP1       [Stack : 0x80, 0x02, 0x40, 0x03, 0xc0, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]\r\n\r\n// 交换栈中第五和第一个元素\r\nSWAP4       [Stack : 0xc0, 0x02, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01                            ]\r\n\r\n// 将 `b` 的值存储到 NFMA 地址的内存中\r\nMSTORE      [Stack : 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02   ]\r\n\r\n// 另一个偏移量用于计算 NFMA\r\nPUSH1(0x60) [Stack : 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02         ]\r\n\r\n// 复制偏移量\r\nDUP1        [Stack : 0x60, 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]\r\n\r\n// 复制 SNFMA\r\nDUP5        [Stack : 0x80, 0x60, 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02                     ]\r\n\r\n// 通过 SNFMA + 偏移量 (0x80 + 0x60) 计算 NFMA\r\nADD         [Stack : 0xe0, 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]\r\n\r\n// 交换栈中第四和第一个元素\r\nSWAP3       [Stack : 0x03, 0x60, 0x40, 0xe0, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]\r\n\r\n// 交换栈中第二和第一个元素\r\nSWAP1       [Stack : 0x60, 0x03, 0x40, 0xe0, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]\r\n \r\n// 交换第 4 个和第 1 个栈元素\r\nSWAP3       [Stack : 0xe0, 0x03, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02               ]\r\n\r\n// 将 `c` 的值存储到 NFMA 的内存中\r\nMSTORE      [Stack : 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19          ]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 注意：所有输入参数现在都存储在内存中\r\n\r\n// 复制 FMPA\r\nDUP1        [Stack : 0x40, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19    ]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 通过读取 FMPA 来加载当前下一个可用内存地址 (SNFMA)\r\nMLOAD       [Stack : 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19    ]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 复制 SNFMA\r\n// 注意：后续操作码与 `abi.encode` 相关\r\nDUP1        [Stack : 0x80, 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]\r\n\r\n// 复制 SNFMA\r\nDUP5        [Stack : 0x80, 0x80, 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03        ]\r\n\r\n// 将第 2 个元素从第 1 个中减去\r\nSUB         [Stack : 0x00, 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]\r\n\r\n// 交换第 2 个和第 1 个栈元素\r\nSWAP1       [Stack : 0x80, 0x00, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]\r\n\r\n// 交换第 4 个和第 1 个栈元素\r\nSWAP3       [Stack : 0x60, 0x00, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]\r\n\r\n// 计算 `size` 参数，用于后续 keccak256 调用\r\nADD         [Stack : 0x60, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19    ]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 复制 SNFMA\r\nDUP3        [Stack : 0x80, 0x60, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19]\r\n            [Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03  ]\r\n\r\n// 将 `size` 参数 (0x60) 存储在 SNFMA 中\r\n// 后续将用于 keccak256 调用\r\nMSTORE      [Stack : 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19                       ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 将 SNFMA 压入栈中\r\nPUSH1(0x80) [Stack : 0x80, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19                 ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 交换第 2 个和第 1 个栈元素\r\nSWAP1       [Stack : 0x40, 0x80, 0x80, 0x80, 0x20, 0x6f, 0x05536b19                 ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 交换第 4 个和第 1 个栈元素\r\nSWAP3       [Stack : 0x80, 0x80, 0x80, 0x40, 0x20, 0x6f, 0x05536b19                 ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 计算 NFMA\r\nADD         [Stack : 0x100, 0x80, 0x40, 0x20, 0x6f, 0x05536b19                      ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 交换第 2 个和第 1 个栈元素\r\nSWAP1       [Stack : 0x80, 0x100, 0x40, 0x20, 0x6f, 0x05536b19                      ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 交换第 3 个和第 1 个栈元素\r\nSWAP2       [Stack : 0x40, 0x100, 0x80, 0x20, 0x6f, 0x05536b19                      ]\r\n            [Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 用下一个 NFMA 覆盖 FMPA 的值\r\nMSTORE      [Stack : 0x80, 0x20, 0x6f, 0x05536b19                                    ]\r\n            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 注意：普通版本必须计算第二个额外的 NFMA\r\n// 并在 FMPA 更新内存；优化版本没有这样做\r\n\r\n// 复制 SNFMA\r\nDUP1        [Stack : 0x80, 0x80, 0x20, 0x6f, 0x05536b19                              ]\r\n            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 将存储在 SNFMA 中的值放入栈中\r\n// 这将是用于 keccak256 的 `size` 参数\r\n// 调用之前计算的\r\nMLOAD       [Stack : 0x60, 0x80, 0x20, 0x6f, 0x05536b19                              ]\r\n            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 交换第 3 个和第 1 个栈元素\r\nSWAP2       [Stack : 0x20, 0x80, 0x60, 0x6f, 0x05536b19                              ]\r\n            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 计算第一个参数的内存地址；用作\r\n// keccak256 的 `offset`\r\nADD         [Stack : 0xa0, 0x60, 0x6f, 0x05536b19                                    ]\r\n            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 调用 keccak256(offset, size)\r\nKECCAK256   [result, 0x6f, 0x05536b19                                                ]\r\n            [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n```\r\n\r\n## Gas 使用比较\r\n\r\n与汇编版本相比，Solidity 版本：\r\n\r\n* 未在起始下一个可用内存地址 (SNFMA) 中存储第一个输入参数 `a`\r\n    \r\n* 相反，它计算了 3 个额外的下一个可用内存地址 (NFMA)，并用于存储输入参数 `a, b, c`\r\n    \r\n* 一旦完成，与汇编版本的 3 个操作码相比，增加了 22 个操作码\r\n    \r\n* 计算了用于 `keccak256` 的 `offset` 参数，而汇编版本中是硬编码的\r\n    \r\n* 更新了自由内存指针地址 (FMPA)，而汇编版本不需要这样做，即使它没有使用更新的地址 (`0x100`)\r\n    \r\n* 总共执行了 48 个操作码，而汇编版本为 20 个操作码，包括 3 个 `MLOAD` 和 5 个 `MSTORE`\r\n    \r\n* 使用了 204 gas，而汇编版本为 117，导致 gas 使用增加了 74% (204-117=87, (87/117)\\*100 = 74)\r\n    \r\n* 因此，汇编版本相比于 Solidity 版本节省了 42% 的 gas (204-117=87, (87/204)*100 = 42)\r\n\r\n## 用 --via-ir 编译有帮助吗？\r\n\r\n使用 --via-ir 编译为相关代码提供了适度的 gas 改进：\r\n\r\n* 汇编版本使用 108 gas，从 117 gas 降低\r\n    \r\n* Solidity 版本使用 195 gas，从 204 gas 降低\r\n    \r\n\r\n相关执行跟踪如下。\r\n\r\n## 执行跟踪(trace) - 汇编 --via-ir 版本\r\n\r\n执行此命令：`forge test --match-contract GasDebugTest --debug test_GasImplAssembly --via-ir`\r\n\r\n我们关心 `GasImplAssembly` 合约内部的执行：\r\n\r\n\r\n*   从第一个输入参数 `a` 的第一个 `CALLDATALOAD` 开始\r\n    \r\n*   直到调用 `keccak256` 并将计算出的哈希放入堆栈\r\n    \r\n\r\n上述执行从 PC 0x32 (50) 开始到 0x46 (70)，使用了 213-105 = 108 gas：\r\n\r\n```\r\n    // 从 calldata 将 `a` 的值推送到堆栈\r\n    CALLDATALOAD [Stack : 0x01]    \r\n                 [Memory:     ]\r\n    \r\n    // 将下一个自由内存地址（SNFMA）推送到堆栈\r\n    PUSH1(0x80)  [Stack : 0x80, 0x01]\r\n                 [Memory:           ]\r\n    \r\n    // 将 `a` 的值存储到 SNFMA 的内存中\r\n    MSTORE       [Stack :            ]\r\n                 [Memory: 0x80 = 0x01]\r\n    \r\n    // 将偏移量推送到堆栈以读取下一个输入变量\r\n    PUSH1(0x24)  [Stack : 0x24       ]\r\n                 [Memory: 0x80 = 0x01]\r\n    \r\n    // 从 calldata 将 `b` 的值推送到堆栈\r\n    CALLDATALOAD [Stack : 0x02       ]\r\n                 [Memory: 0x80 = 0x01]\r\n    \r\n    // 将下一个自由内存地址（NFMA）推送到堆栈 \r\n    PUSH1(0xa0)  [Stack : 0xa0, 0x02 ]\r\n                 [Memory: 0x80 = 0x01]\r\n    \r\n    // 注意：--via-ir 能够预计算自由内存地址\r\n    // 这样它不必在运行时计算它们，而可以直接将它们推送到堆栈\r\n    \r\n    // 将 `b` 的值存储到 NFMA 的内存中\r\n    MSTORE       [Stack :                         ]\r\n                 [Memory: 0x80 = 0x01, 0xa0 = 0x02]\r\n    \r\n    // 将偏移量推送到堆栈以读取下一个输入变量\r\n    PUSH1(0x44)  [Stack : 0x44                    ]\r\n                 [Memory: 0x80 = 0x01, 0xa0 = 0x02]\r\n    \r\n    // 从 calldata 将 `c` 的值推送到堆栈\r\n    CALLDATALOAD [Stack : 0x03                    ]\r\n                 [Memory: 0x80 = 0x01, 0xa0 = 0x02]\r\n    \r\n    // 将下一个自由内存地址（NFMA）推送到堆栈 \r\n    PUSH1(0xc0)  [Stack : 0xc0, 0x03              ]\r\n                 [Memory: 0x80 = 0x01, 0xa0 = 0x02]\r\n    \r\n    // 将 `c` 的值存储到 NFMA 的内存中\r\n    MSTORE       [Stack :                                      ]\r\n                 [Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]\r\n    \r\n    // 注意：所有输入参数现在都存储在内存中\r\n    \r\n    // 推送 `size` 参数以便调用 keccak 到堆栈\r\n    PUSH1(0x60)  [Stack : 0x60                                 ]\r\n                 [Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]\r\n    \r\n    // 推送 `offset` 参数以便调用 keccak 到堆栈\r\n    PUSH1(0x80)  [Stack : 0x80, 0x60                           ]\r\n                 [Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]\r\n    \r\n    // 调用 keccak256(offset, size)\r\n    KECCAK256   [Stack : result                               ]\r\n                [Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]\r\n    \r\n```\r\n## 执行跟踪 - Solidity --via-ir 版本\r\n\r\n执行此命令： `forge test --match-contract GasDebugTest --debug test_GasImplNormal --via-ir`\r\n\r\n我们关注的是 `GasImplNormal` 合约内的执行：\r\n\r\n*   从第一个输入参数 `a` 的第一个 `CALLDATALOAD` 开始\r\n    \r\n*   直到调用 `keccak256` 并将计算出的哈希放入堆栈\r\n    \r\n\r\n上述执行从 PC 0x3a (58) 开始到 0x72 (114)，使用了 318-123 = 195 gas：\r\n```\r\n    // 从 calldata 将 `a` 的值推送到堆栈\r\n    CALLDATALOAD [Stack : 0x01, 0xa0, 0x80, 0x00]    \r\n                 [Memory:                       ]\r\n    \r\n    // 将下一个自由内存地址（NFMA）重复推送到堆栈\r\n    DUP2         [Stack : 0x0a, 0x01, 0xa0, 0x80, 0x00]    \r\n                 [Memory:                             ]\r\n    \r\n    // 将 `a` 的值存储到 NFMA 的内存中\r\n    MSTORE       [Stack : 0xa0, 0x80, 0x00]    \r\n                 [Memory: 0xa0 = 0x01     ]\r\n    \r\n    // 将偏移量推送到堆栈以读取下一个输入变量\r\n    PUSH1(0x24)  [Stack : 0x24, 0xa0, 0x80, 0x00]\r\n                 [Memory: 0xa0 = 0x01           ]\r\n    \r\n    // 从 calldata 将 `b` 的值推送到堆栈\r\n    CALLDATALOAD [Stack : 0x02, 0xa0, 0x80, 0x00]\r\n                 [Memory: 0xa0 = 0x01           ]\r\n    \r\n    // 将自由内存指针地址（FMPA）推送到堆栈 \r\n    PUSH1(0x40)  [Stack : 0x40, 0x02, 0xa0, 0x80, 0x00]\r\n                 [Memory: 0xa0 = 0x01                 ]\r\n    \r\n    // 重复 0x80\r\n    DUP4         [Stack : 0x80, 0x40, 0x02, 0xa0, 0x80, 0x00]\r\n                 [Memory: 0xa0 = 0x01                       ]\r\n    \r\n    // 计算下一个自由内存地址（NFMA）\r\n    ADD          [Stack : 0xc0, 0x02, 0xa0, 0x80, 0x00]\r\n                 [Memory: 0xa0 = 0x01                 ]\r\n    \r\n    // 将 `b` 的值存储到 NFMA 的内存中\r\n    MSTORE       [Stack : 0xa0, 0x80, 0x00        ]    \r\n                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02] \r\n    \r\n    // 将偏移量推送到堆栈以读取下一个输入变量\r\n    PUSH1(0x44)  [Stack : 0x44, 0xa0, 0x80, 0x00  ]\r\n                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02]\r\n    \r\n    // 从 calldata 将 `c` 的值推送到堆栈\r\n    CALLDATALOAD [Stack : 0x03, 0xa0, 0x80, 0x00  ]\r\n                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02]\r\n    \r\n    // 推送偏移量以计算 NFMA\r\n    PUSH1(0x60)  [Stack : 0x60, 0x03, 0xa0, 0x80, 0x00]\r\n                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02    ]\r\n    \r\n    // 重复 0x80\r\n    DUP4         [Stack : 0x80, 0x60, 0x03, 0xa0, 0x80, 0x00]\r\n                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02          ]\r\n    \r\n    // 计算 NFMA\r\n    ADD          [Stack : 0xe0, 0x03, 0xa0, 0x80, 0x00]\r\n                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02    ]\r\n    \r\n    // 将 `c` 的值存储到 NFMA 的内存中\r\n    MSTORE       [Stack : 0xa0, 0x80, 0x00                     ]\r\n                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n    \r\n    // 注意：所有输入参数现在都存储在内存中\r\n    \r\n    // 推送 0x60，将保存到内存，并将在后面\r\n    // 用作 keccak256 调用的 `size` 参数\r\n    PUSH1(0x60)  [Stack : 0x60, 0xa0, 0x80, 0x00               ]\r\n                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n    \r\n    // 注意：后续与 `abi.encode` 相关的操作码\r\n    \r\n    // 复制内存地址以保存先前推送的 `size` 参数\r\n    DUP3         [Stack : 0x80, 0x60, 0xa0, 0x80, 0x00         ]\r\n                 [Memory: 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n    \r\n    // 将 `size` 参数保存到内存中\r\n    MSTORE       [Stack : 0xa0, 0x80, 0x00                                  ]\r\n                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n    \r\n    // 用于计算 NFMA\r\n    PUSH1(0x80)  [Stack : 0x80, 0xa0, 0x80, 0x00                            ]\r\n                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n    \r\n    // 用于计算 NFMA\r\n    DUP3         [Stack : 0x80, 0x80, 0xa0, 0x80, 0x00                      ]\r\n                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n    \r\n    // 计算 NFMA；该值用于 LT 和 GT 比较\r\n    // 然后稍后保存到 FMPA\r\n    ADD          [Stack : 0x100, 0xa0, 0x80, 0x00                           ]\r\n                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n    \r\n    // 交换第 3 和第 1 个堆栈元素\r\n    SWAP2        [Stack : 0x80, 0xa0, 0x100, 0x00                           ]\r\n                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n    \r\n    // 用于 LT 比较\r\n    DUP1         [Stack : 0x80, 0x80, 0xa0, 0x100, 0x00                     ]\r\n                 [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n\r\n// 用于 LT 比较\r\nDUP4         [Stack : 0x100, 0x80, 0x80, 0xa0, 0x100, 0x00              ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 0x100 < 0x80 ? false\r\nLT           [Stack : 0x00, 0x80, 0xa0, 0x100, 0x00                     ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 用于 GT 比较\r\nPUSH8(0xffffffffffffffff)\r\n             [Stack : 0xffffffffffffffff, 0x00, 0x80, 0xa0, 0x100, 0x00 ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 复制\r\nDUP5         [Stack : 0x100, 0xffffffffffffffff, 0x00, 0x80, 0xa0, 0x100, 0x00]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03      ]\r\n\r\n// 0x100 > 0xffffffffffffffff ? false\r\nGT           [Stack : 0x00, 0x00, 0x80, 0xa0, 0x100, 0x00               ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 0x00 或 0x00 = 0x00\r\nOR           [Stack : 0x00, 0x80, 0xa0, 0x100, 0x00                     ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 不确定为什么这里会被推送\r\nPUSH1(0x76)  [Stack : 0x76, 0x00, 0x80, 0xa0, 0x100, 0x00               ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 跳转？ false\r\nJUMPI        [Stack : 0x80, 0xa0, 0x100, 0x00                           ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 不确定为什么这里会被推送\r\nPUSH1(0x20)  [Stack : 0x20, 0x80, 0xa0, 0x100, 0x00                     ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 交换第 5 个和第 1 个栈元素\r\nSWAP4        [Stack : 0x00, 0x80, 0xa0, 0x100, 0x20                     ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 移除顶部元素 0x00\r\nPOP          [Stack : 0x80, 0xa0, 0x100, 0x20                           ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 复制 \r\nDUP3         [Stack : 0x100, 0x80, 0xa0, 0x100, 0x20                    ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 准备用先前计算的 NFMA 覆盖 FMPA\r\nPUSH1(0x40)  [Stack : 0x40, 0x100, 0x80, 0xa0, 0x100, 0x20              ]\r\n             [Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 用下一个 NFMA 覆盖 FMPA 的值\r\n// 但 FMPA 并没有被使用？\r\nMSTORE       [Stack : 0x80, 0xa0, 0x100, 0x20                                         ]\r\n             [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 加载 keccak256 调用的 `size` 参数\r\nMLOAD        [Stack : 0x60, 0xa0, 0x100, 0x20                                         ]\r\n             [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 交换第 2 个和第 1 个栈元素\r\nSWAP1        [Stack : 0xa0, 0x60, 0x100, 0x20                                         ]\r\n             [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n\r\n// 调用 keccak256(offset, size)\r\nKECCAK256    [Stack : result, 0x100, 0x20                                             ]\r\n             [Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]\r\n```\r\n\r\n##  使用 Halmos 进行形式验证\r\n\r\n可以将以下函数添加到现有的 `GasDebugTest` 合约中，以便正式验证使用 [Halmos](https://github.com/a16z/halmos) 证明汇编和 Solidity 版本生成相同的输出：\r\n\r\n```\r\n// halmos --match-contract GasDebugTest\r\nfunction check_GasImplEquivalent(uint256 a1, uint256 b1, uint256 c1) external {\r\n    bytes32 resultNormal   = gasImplNormal.getKeccak256(a1,b1,c1);\r\n    bytes32 resultAssembly = gasImplAssembly.getKeccak256(a1,b1,c1);\r\n\r\n    assertEq(resultNormal, resultAssembly);\r\n}\r\n```\r\n\r\n执行形式验证使用：`halmos --match-contract GasDebugTest`\r\n\r\n \r\n> 我是 [AI 翻译官](https://learnblockchain.cn/people/19584)，为大家转译优秀英文文章，如有翻译不通的地方，在[这里](https://github.com/lbc-team/Pioneer/blob/master/translations/10520.md)修改，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/24434","address":null},"history":"bafkreibk5d3c5yyaekud3kgxw2g55vgtdsxarydiz2v5yji33kz3cn7m7q","timestamp":1736148351,"version":1}