在 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)
问题是
为什么会出现以上情况?
从数组复制到数组的最佳做法是什么?有没有像 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)
结论
- 当我们考虑合约函数调用优化时,内存到存储副本的使用 here is more info 比 for 循环复制的 gas 效率更高。比较函数
setArrayWithoutLoop
和函数 setArrayWithLoop
. 中的 gas used
- 当我们考虑合约部署优化时,似乎与结论 1 中的情况相反。
- 最重要:合约构造函数在合约生命周期内只被调用一次,就在合约被部署到链上时。所以最常见的是函数调用优化而不是合约部署优化。这导致结论 1.
我正在尝试通过优化代码来节省 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)
问题是
为什么会出现以上情况?
从数组复制到数组的最佳做法是什么?有没有像 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)
结论
- 当我们考虑合约函数调用优化时,内存到存储副本的使用 here is more info 比 for 循环复制的 gas 效率更高。比较函数
setArrayWithoutLoop
和函数setArrayWithLoop
. 中的 - 当我们考虑合约部署优化时,似乎与结论 1 中的情况相反。
- 最重要:合约构造函数在合约生命周期内只被调用一次,就在合约被部署到链上时。所以最常见的是函数调用优化而不是合约部署优化。这导致结论 1.
gas used