介绍了如 Solidity 智能合约中使用内联汇编语言(Inline Assembly)实现keccak256哈希函数的优化方法.
- 原文链接:dacian.me/solidity-ass...
 - 译者:AI翻译官,校对:翻译小组
 - 本文链接:learnblockchain.cn/article…
 
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 测试合约:
    // 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);
        }
    }
接下来,我们将使用 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
在检查了汇编版本后,我们现在将转向使用 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]
与汇编版本相比,Solidity 版本:
未在起始下一个可用内存地址 (SNFMA) 中存储第一个输入参数 a
相反,它计算了 3 个额外的下一个可用内存地址 (NFMA),并用于存储输入参数 a, b, c
一旦完成,与汇编版本的 3 个操作码相比,增加了 22 个操作码
计算了用于 keccak256 的 offset 参数,而汇编版本中是硬编码的
更新了自由内存指针地址 (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 编译为相关代码提供了适度的 gas 改进:
汇编版本使用 108 gas,从 117 gas 降低
Solidity 版本使用 195 gas,从 204 gas 降低
相关执行跟踪如下。
执行此命令: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]
执行此命令: 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]
可以将以下函数添加到现有的 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
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!