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

5

我正在尝试通过优化代码来节省气体。然而,突然想知道在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;
    }
}

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 with Enable optimization (200)进行了测试。

测试结果讨论

我们可以看到,TestOne执行所需的气体为152253 gas,而TestTwo执行所需的气体为150792 gas

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

我怀疑solidity编译器的“优化”。但是,在没有“启用优化”的情况下进行相同的小实验后,它得出了相同的结果,即for循环使用的gas较少。(198846 vs. 198464)

问题是

  1. 为什么会发生上述情况?

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

1个回答

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;
    }
}

使用 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. 当我们考虑合约函数调用优化时,使用内存进行存储复制比通过循环复制更加节省 gas。请参考此处,比较函数 setArrayWithoutLoop 和函数 setArrayWithLoopgas used
  2. 当我们考虑合约部署优化时,与结论1相反。
  3. 最重要的是:合约构造函数仅在合约生命周期中调用一次,即在合约部署到链上时。因此,通常情况下会进行函数调用优化而不是合约部署优化。这导致了结论1。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接