在 Solidity 中从一个数组复制到另一个数组的最佳实践是什么?

What is the best practice of copying from array to array in Solidity?

我正在尝试通过优化代码来节省 gas。然而,一瞬间,我想知道在 Solidity 中从一个数组复制到另一个数组的最佳实践是什么。

我提供两个选项。一种是通过指针复制(我猜),另一种是使用 for-loop。

TestOne.sol

contract TestContract {
    uint32[4] testArray;

    constructor(uint32[4] memory seeds) {
        testArray = seeds; // execution costs: 152253
    }

    function Show() public returns (uint32[4] memory) {
        return testArray;
    }
}

TestTwo.sol

contract TestContract {
    uint32[4] testArray;

    constructor(uint32[4] memory seeds) {
        for(uint i = 0; i < 4; i++) {
            testArray[i] = seeds[i];  // execution costs: 150792
        }
    }

    function Show() public returns (uint32[4] memory) {
        return testArray;
    }
}

我使用 Remix(Ethereum Online IDE)、0.8.13 Solidity Compiler 和 Enable optimization (200) 进行了测试

测试结果讨论

我们可以看到,TestOne使用了152253 gas执行成本,TestTwo使用了150792 gas执行成本

有趣的是,for-loop 比仅仅分配指针使用更少的 gas。在我看来,for-loop 会比其他的有更多的汇编代码。 (至少会有赋值uint i,替换4次,检查条件4次(是否i < 4),增加i++ 4次等)

怀疑solidity编译器的“优化”。但是,在没有“启用优化”的情况下进行相同的小实验后,for-loop 使用更少的 gas 的结果相同。 (198846 对 198464)

问题是

  1. 为什么会出现以上情况?

  2. 从数组复制到数组的最佳做法是什么?有没有像 C++ 的 std::copy() 那样的复制函数?

最佳做法是将数组从内存复制到存储而不循环遍历它们的项目。但是,此示例中的合同优化很棘手。 official documentation 表示如下:

If you want the initial contract deployment to be cheaper and the later function executions to be more expensive, set it to --optimize-runs=1. If you expect many transactions and do not care for higher deployment cost and output size, set --optimize-runs to a high number.

为了说明以上内容,请考虑以下合同:

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

contract TestLoop {
    uint32[4] testArray;

    function setArrayWithLoop(uint32[4] memory array) public {
        for(uint256 i = 0; i < array.length; i++)
            testArray[i] = array[i];
    }

    function setArrayWithoutLoop(uint32[4] memory array) public {
        testArray = array;
    }

    function show() public view returns (uint32[4] memory) {
        return testArray;
    }
}

contract NoLoop {
    uint32[4] testArray;

    constructor(uint32[4] memory array) {
        testArray = array;
    }

    function show() public view returns (uint32[4] memory) {
        return testArray;
    }
}

contract Loop {
    uint32[4] testArray;

    constructor (uint32[4] memory array) {
        for(uint256 i = 0; i < array.length; i++)
            testArray[i] = array[i];
    }

    function show() public view returns (uint32[4] memory) {
        return testArray;
    }
}

和使用brownie编写的脚本:

from brownie import TestLoop, NoLoop, Loop, accounts

def function_calls():
    contract = TestLoop.deploy({'from': accounts[0]})
    print('set array in loop')
    contract.setArrayWithLoop([1, 2, 3, 4], {'from': accounts[1]})
    print('array ', contract.show(), '\n\n')

    print('set array by copy from memory to storage')
    contract.setArrayWithoutLoop([10, 9, 8, 7], {'from': accounts[2]})
    print('array ', contract.show(), '\n\n')

def deploy_no_loop():
    print('deploy NoLoop contract')
    contract = NoLoop.deploy([21, 22, 23, 24], {'from': accounts[3]})
    print('array ', contract.show(), '\n\n')

def deploy_loop():
    print('deploy Loop contract')
    contract = Loop.deploy([31, 32, 33, 34], {'from': accounts[3]})
    print('array ', contract.show(), '\n\n')

def main():
    function_calls()
    deploy_no_loop()
    deploy_loop()

与以下 brownie-config.yaml:

compiler:
  solc:
    version: 0.8.13
    optimizer:
      enabled: true
      runs: 1

给出以下输出:

Running 'scripts/test_loop.py::main'...
Transaction sent: 0x8380ef4abff179f08ba9704826fc44961d212e5ee10952ed3904b5ec7828c928
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  TestLoop.constructor confirmed   Block: 1   Gas used: 251810 (2.10%)
  TestLoop deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87

set array in loop
Transaction sent: 0xfe72d6c878a980a9eeefee1dccdd0fe8214ee4772ab68ff0ac2b72708b7ab946
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  TestLoop.setArrayWithLoop confirmed   Block: 2   Gas used: 49454 (0.41%)

array  (1, 2, 3, 4) 


set array by copy from memory to storage
Transaction sent: 0x0106d1a7e37b155993a6d32d5cc9dc67696a55acd1cf29d2ed9dba0770436b98
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  TestLoop.setArrayWithoutLoop confirmed   Block: 3   Gas used: 41283 (0.34%)

array  (10, 9, 8, 7) 


deploy NoLoop contract
Transaction sent: 0x55ddded68300bb8f11b3b43580c58fed3431a2823bf3f82f0081c7bfce66f34d
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  NoLoop.constructor confirmed   Block: 4   Gas used: 160753 (1.34%)
  NoLoop deployed at: 0x7CA3dB74F7b6cd8D6Db1D34dEc2eA3c89a3417ec

array  (21, 22, 23, 24) 


deploy Loop contract
Transaction sent: 0x1aa64f2cd527983df84cfdca5cfd7a281ff904cca227629ec8b0b29db561c043
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 1
  Loop.constructor confirmed   Block: 5   Gas used: 153692 (1.28%)
  Loop deployed at: 0x2fb0fE4F05B7C8576F60A5BEEE35c23632Dc0C27

array  (31, 32, 33, 34)

结论

  1. 当我们考虑合约函数调用优化时,内存到存储副本的使用 here is more info 比 for 循环复制的 gas 效率更高。比较函数 setArrayWithoutLoop 和函数 setArrayWithLoop.
  2. 中的 gas used
  3. 当我们考虑合约部署优化时,似乎与结论 1 中的情况相反。
  4. 最重要:合约构造函数在合约生命周期内只被调用一次,就在合约被部署到链上时。所以最常见的是函数调用优化而不是合约部署优化。这导致结论 1.