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

Tye*_*Rik 5 arrays ethereum solidity truffle remix

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

我提出两个选择。一种是通过指针复制(我猜),另一种是使用 for 循环。

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

测试二.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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我使用 Remix(以太坊在线 IDE)、0.8.13 Solidity 编译器和启用优化进行了测试(200)

测试结果讨论

我们可以看到,TestOne 使用了152253 Gas作为执行成本,TestTwo 使用了150792 Gas作为执行成本。

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

我怀疑solidity编译器的“优化”。但是,在没有“启用优化”的情况下进行相同的小实验后,它会得到与 for 循环使用更少的气体相同的结果。(198846 与 198464)

问题是

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

  2. 从数组复制到数组的最佳实践是什么?有没有类似C++的复制功能std::copy()

小智 4

最佳实践是将数组从内存复制到存储,而不循环遍历其项目。然而,这个例子中的合约优化很棘手。官方文档是这样说的:

如果您希望初始合约部署更便宜,而后续函数执行更昂贵,请将其设置为--optimize-runs=1。如果您预计会有很多事务并且不关心较高的部署成本和输出大小,请设置--optimize-runs为较高的数字。

为了说明上述情况,请考虑以下合同:

// 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

和使用以下方式编写的脚本brownie

// 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

与以下brownie-config.yaml

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()
Run Code Online (Sandbox Code Playgroud)

给出以下输出:

compiler:
  solc:
    version: 0.8.13
    optimizer:
      enabled: true
      runs: 1
Run Code Online (Sandbox Code Playgroud)

结论

  1. 当我们考虑合约函数调用优化时,使用内存来存储副本(这里有更多信息)比 for 循环复制的 Gas 效率更高。gas used从功能setArrayWithoutLoop和功能上比较setArrayWithLoop
  2. 当我们考虑合约部署优化时,结论 1 中似乎存在与此相反的情况。
  3. 最重要的是:合约构造函数在合约生命周期中仅被调用一次,即当合约部署到链上时。所以最常见的是函数调用优化而不是合约部署优化。由此得出结论1。