견고성 : 가스 최적화
목차
소개
When writing smart contract in Solidity, you may not be really concerned about the gas consumption in Remix environment or in a testnet. You only realise how much cost every OPCODES when you deploy in the mainnet on Ethereum (or in L2 but it's quite cheap).
Today, we will cover some optimisation we can do in Solidity. This list is not exhaustive and feel free to test every option you can.
온체인 또는 오프체인 데이터
As mentioned above, writing state variable cost you some gas. That's why you should be aware that every user's call can cost a lot. You should be asking : "Do I really need to store this information on chain ?"
Every data related to the "ecosystem" or that doesn't need to be censored should be inside your smart contract. When it comes to metadata, files you can handle them off chain.
견고성 옵티마이저
It's a mode that enable to produce a more efficient bytecode by reducing, the code size or the gas needed for the deployment. Keep in mind that the optimiser is not turned on by default because it takes more time to compile the actual solidity code which is not efficient in development mode. Turn on the optimiser when you want to deploy your smart contract
EVM 슬롯
You have probably seen or heard someone telling you to order your state variables by type ?
This is not without any reason. The EVM stores the data of a smart contract into a memory slots of 256 bits. A uint256 will fill one slot. But a uint128 will fill 128 bits over 256. if the next state variable is a uint256 then, the variable will be stored in the next slot. The EVM will perform a computation to force the previous slots to fit the remaining space.
Take this code :
uint128 variable_1
uint128 variable_2
uint256 variable_3
variable_1
will be stored in slot 0. Then variable_2
can fit inside the slot 0 because it has a remaining space of 128 bits.
After that, the variable_3
will be stored in slot 1.
In a not optimised version we can have this code
address address_1
uint128 variable_1
uint256 variable_2
address address_2
uint128 variable_3
address_1
is 160 bits long (address is a 20 bytes type). It will be stored in slot 0. variable_1
is 128 bits. But 128 + 160 = 288 bits. It can't fit in the 96 bits remaining space. (256-160). Then, variable_1
will be stored in slot 1. variable_2
is a 256 bits length. It will be stored in slot 2 because it can't fit in the slot 1 (128 bits remaining space). address_2
will be in slot 3 because the slot 2 is full. The variable_3
will be in slot 4 because the slot 3 has already 160 bits reserved (96 remaining).
To optimise this version, we can adjust the variable as follow :
uint128 variable_1
uint128 variable_3
uint256 variable_2
address address_1
address address_2
Here, there is 4 slots taken while the non-optimised version has 5 slots taken. We have saved 32 bytes and prevent unnecessary computation.
배열을 통한 루프
Arrays are quite common nowadays and it's pretty straightforward to read values from them with a for-loop.
If you have guessed where I'm going by saying that, it means you read carefully the introduction.
If you read an array which is a state variable, looping inside the array without caching it into a memory variable cost you more gas.
In the following example, you can see that for a 9-length array, we spent 83037 gas in the un-optimised version and only 56475 in the most optimised
My advice: Avoid looping through an array that has a dynamic length. The gas consumption increases overtime and it will become a very expensive process.
(Try it on Remix)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract GasTest {
uint[] public arrayTest;
uint public sumArray;
constructor(){
arrayTest = [1,2,3,4,5,6,7,8,9];
}
/**
* Keep reading the value of the array from the state variable
* Keep writing the result in the state variable sumArray
* Bad idea
* Gas cost = 83037
*/
function calculateSum() external {
for(uint i = 0; i<arrayTest.length; i++){
sumArray += arrayTest[i];
}
}
/**
* Keep reading the value of the array from the state variable
* Caching the sum in the memory. Write only once in the state variable
* Not an excellent idea
* Gas cost = 58087
*/
function calculateSum2() external {
uint _sumArray = 0;
for(uint i = 0; i<arrayTest.length; i++){
_sumArray += arrayTest[i];
}
sumArray = _sumArray;
}
/**
* Caching the array in the memory
* Caching the sum in the memory. Write only once in the state variable
* Excellent idea
* Gas cost = 56475
*/
function calculateSum3() external {
uint _sumArray = 0;
uint[] memory _arrayTest = arrayTest;
for(uint i = 0; i<_arrayTest.length; i++){
_sumArray += _arrayTest[i];
}
sumArray = _sumArray;
}
}
도서관
If you have many smart contracts using the same functionalities, you should extract all these functionalities to a library and deploy it.
Now, all the smart contracts can reference the deployed library instead of deploying the same code again and again and increase the bytecode length.
Because it's a deployed library, make sure that all your functions are external
이벤트
If you have data created on a smart contract but does not need to be read after by the smart contract, then events are a good candidate. Events consume less gas than a state variable.
계산된 값
Sometimes, you need to store values into the smart contract (hashes for example) and you need to compute this value. My advice is to compute this value off chain and instantiate your smart contract with the computed value. (Remember: The less OPCODE you have, the better optimisation you will get)
상수
You can have some constant values in your state variables that won't change. Constant values can be useful for gas optimisation.
Here an example:
- Read state variable with const: 21420 gas
- Read state variable without const: 23541 gas
(Try it on Remix)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract GasTestConst {
//Gas cost : 21420
address public constant CONST_ADDR = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
}
contract GasTestNoConst {
//Gas Cost: 23541
address public addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
}
You can say that immutable
works the same as const
. The only difference between immutable
and const
is that you can initialise the value of the immutable
variable into the constructor.
오류 처리
When you use require
, you pass the error reason if the condition is not met. But very long string cost more gas.
Using a custom error cost less gas. For the example below, I put the same string.
Remember that with custom error, you should log the value.
The gas cost is just slightly different but after a while, it saves some money.
(Try it on Remix)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract GasTestError {
error CustomError(string message);
//Gas cost: 21977
function testRequire(uint _val) public pure {
require(_val <= 2, "Sorry but your value does not meet the requirement. Please select a value between 0 and 2. Otherwise... Good bye");
//process
}
//Gas cost: 21955
function testError(uint _val) public pure {
if(_val > 2){
revert CustomError("Sorry but your value does not meet the requirement. Please select a value between 0 and 2. Otherwise... Good bye");
}
//process
}
}
결론
이 목록은 완전하지 않습니다. 프록시 계약을 위해 EIP 1167을 추가할 수도 있지만 고급 기술을 설명하고 싶지는 않습니다. 이 10가지 팁을 시도하고 스마트 계약을 활용하십시오. 목표는 초보자를 위한 기본 최적화로 약간의 가스를 절약하는 것입니다.
질문이 있으시면 아래에 댓글을 달아주세요 ;)
Reference
이 문제에 관하여(견고성 : 가스 최적화), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/mousticke/solidity-gas-optimisation-5425텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)