Solidity의 가스 절약 기술

Gas은 Solidity의 계산 측정 단위입니다. 스마트 계약과 상호 작용하고 거래하기 위해 지불하는 금액입니다. gas은 다음과 같은 간단한 공식을 통해 계산됩니다.Gas = Gas Price * Gas used .gas 가격은 일정하지 않으며 네트워크 정체에 따라 달라집니다. 네트워크를 통해 거래를 추진하는 사용자가 많을수록 gas 가격이 높아집니다. 사용된 gas의 양은 스마트 컨트랙트에 의해 수행되는 연산에 의해 결정됩니다.

이 블로그 게시물에서는 트랜잭션을 스마트 계약으로 보낼 때 소비되는 gas을 줄이기 위해 사용할 수 있는 몇 가지 기술에 대해 설명합니다. 우리는 스마트 계약의 gas 풋 프린트를 줄이기 위해 다음 나열된 기술을 고려할 것입니다.

  • Use constant and immutable variables for variable that don't change

  • Cache read variables in memory

  • Incrementing and decrementing by 1

  • calldata and memory

  • Calling view function

  • Integer overflow/underflow

  • Use revert instead of require

  • Avoid loading too many data in memory

  • Other tips

  • 변경되지 않는 변수에 대해 상수 및 불변 변수 사용

    Using the constant and the immutable keywords for variables that do not change helps to save on gas used. The reason been that constant and immutable variables do not occupy a storage slot when compiled. They are saved inside the contract byte code. Lets see an example.

    //SPDX-License-Identifier:MIT
    pragma solidity ^0.8.3;
    
    interface IERC20 {    
     function transfer(address to, uint256 value) external returns (bool);    
    function approve(address spender, uint256 value) external returns (bool);    
    function transferFrom(address from, address to, uint256 value) external returns (bool);   
    //rest of the interface code
    }
    
    //Gas used: 187302
    contract Expensive {
       IERC20 public token;
       uint256 public timeStamp = 566777;
    
       constructor(address token_address) {
           token = IERC20();
       }
    
    }
    
    //Gas used: 142890
    contract GasSaver {
       IERC20 public immutable i_token;
       uint256 public constant TIMESTAMP = 566777;
    
       constructor(address token_address) {
           i_token = IERC20(token_address);
       }
    }
    
    

    The first contract Expensive has a public variable of type IERC20 and a uint256 also defined as public. When we deployed this contract, the contract deployment used up a total of 187302 gas when deployed to Remix. The second contract named GasSaver also have the same variable structure as the Expensive contract but we made use of constant keyword for the TIMESTAMP because we will not be changing the value. For the interface we used the immutable keyword which means we can no longer modify the value of the i_token variable after setting it inside the constructor . This action alone reduced the gas consumption from 187302 to 142890.

    메모리의 캐시 읽기 변수

    Reading from a variable in storage cost gas . When we access a variable for the first time it cost 2100 gas and subsequent access will cost 100 gas . If we have an array and want to perform some computation on the array elements. It could be more gas effective to read the length of the array and store it in a variable. If the array is not too large we could also read the whole array into memory and access it from memory instead of continuously reading from storage.

     //SPDX-License-Identifier:MIT;
    
    pragma solidity ^0.8.3;
    
    contract Expensive {
       uint256[] public numbers;
    
       constructor(uint256[] memory _numbers) {
           numbers = _numbers;
       }
    
       //Gas used: 40146
       function sum() public view returns (uint256 ){
           uint256 total = 0;
           for (uint256 i=0; i < numbers.length; i++) {
              total += numbers[i];
           }
           return total;
       }
    }
    
    contract GasSaver {
       uint256[] public numbers;
    
       constructor(uint256[] memory _numbers) {
           numbers = _numbers;
       }
    
       //39434
       function sum() public view returns (uint256 ){
           uint256 total = 0;
           uint256 arrLength = numbers.length;
           uint256[] memory _numbersInMemory = numbers;
           for (uint256 i=0; i < arrLength; i++) {
               total += _numbersInMemory[i];
           }
           return total;
       }
    }
    

    The contract GasSaver above saved the length of the array in a variable arrLength and also loaded the array into memory. This helps to reduce the gas spent inside the function.

    1씩 증가 및 감소

    There are four ways to increment and decrement a variable by 1 in Solidity. Let us see an example.

    //SPDX-License-Identifier:MIT;
    
    pragma solidity ^0.8.3;
    
    contract One{
       uint256 public number;
      //Gas used : 43800
       function incrementByOne() public returns (uint256){
          number += 1;
          return number;
       }
    }
    
    contract Two{
       uint256 public number;
       //Gas used : 43787
       function incrementByOne() public returns (uint256){
         number = number + 1;
         return number;
       }
    }
    
    contract Three{
       uint256 public number;
       //Gas used : 43634
       function incrementByOne() public returns (uint256){
           return number++;
       }
    }
    
    contract Four{
       uint256 public number;
       //Gas used : 43628
       function incrementByOne() public returns (uint256){
           return ++number;
       }
    }
    
    

    The example above list four contracts which shows the way we could increment a number variable by a value of 1. The contract Four is more gas effective out of the lot when ran. To save more on gas when incrementing a variable use the method of increment in contract Four ( ++number ) which is more gas efficient.

    호출 데이터 및 메모리

    When running a function we could pass the function parameters as calldata or memory for variables such as strings , structs , arrays etc. If we are not modifying the passed parameter we should pass it as calldata because calldata is more gas efficient than memory . Let's see an example:

    //SPDX-License-Identifier:MIT;
    pragma solidity ^0.8.3;
    contract GasSaver {
        //Gas used: 22471
        function passParameterAsCallData(string calldata _name) public returns (string memory){}
        //Gas used: 22913
        function passParameterAsMemory(string memory _name) public returns (string memory){}
    }
    

    Calling these two functions in Remix and passing the same parameter to the functions, you will noticed that setting a function _name parameter as calldata reduced the gas used.

    You can only used calldata when you are not going to modify the value of the variable passed as calldata inside the function.

    뷰 함수 호출

    A view function does not use gas when called, but if we decide to call a view function inside of another function which is a transaction, it then uses gas .

    //SPDX-License-Identifier:MIT;
    
    pragma solidity ^0.8.3;
    contract GasSaver {
        uint256[] private numbers = [2,3,5,67,34];
    
        function getNumberAt( uint256 _index ) public view returns (uint256){
            return numbers[_index];
        }
        //Gas used when getNumber was called: 44778
    
        //Gas used without calling getNumber() 44450
        function sumAndMultiply() public {
           uint256[] memory _numbers = numbers;
           uint256 arrlength = _numbers.length;
           for(uint256 i=0; i < arrlength; ++i){
              numbers[i] = _numbers[i] * i;
           }
           //getNumberAt(2);
        }
    }
    
    

    The above is a simple contract to illustrate the point about calling view functions inside of a transaction. The function getNumberAt is a view function that returns the value of the array at the passed index. Calling this function uses no gas because it is a view function, but if we decide to call this function inside of the sumAndMultiply function, we can see that the gas usage of sumAndMultiply
    has increased because the view function is called within it.

    Gas used when sumAndMutiply functions is called: 44778 .
    Gas used when getNumberAt view function is also called inside the sumAndMultiply function: 44450 .

    So, we can see that calling a view function inside a transaction increased the amount of gas that should have been spent, if the view function was not included in the call.

    정수 오버플로우/언더플로우

    Prior to Solidity 0.8.0 version, interger overflow and underflow checks were performed by using the SafeMath library. From Solidity 0.8.0 upward, the compiler does that check for us.

    This extra check cost gas . If we know that the mathematical operations we will perform inside the contract will not overflow or underflow, we can tell the compiler not to check for overflow or underflow in the operation.

    //SPDX-License-Identifier:MIT;
    
    pragma solidity ^0.8.3;
    
    contract OverFlow {
        uint256 public numberOne;
        uint256 public numberTwo;
    
        //Gas used: 43440
        function setNumberOne() public {
            ++numberOne;
        }
        //Gas used: 43339
         function setNumberTwo() public {
           unchecked {
               ++numberTwo;
           }
        }
    }
    

    The above contract OverFlow has two functions used to set two variables inside the contract. The compiler in setNumberOne checks and prevents the number from overflow or underflow. In the second function setNumberTwo we are telling the compiler to mind its business and not check for overflow or underflow. Using unchecked means that the code block will not be checked for overflow or underflow.

    If we know that our mathematics will be safe we could safe a little gas by using unchecked .

    요구 대신 되돌리기 사용

    We use require to check if a statement is true . If the statement evaluates to false the transaction is reverted and the remaining gas returned to the user. Since the advert of custom error in Solidity we could use the revert statement to throw a custom error. Using revert instead of require is more gas efficient. You can run the code below to see the the amount of gas saved when revert is used instead of require .

    //SPDX-License-Identifier:MIT;
    pragma solidity ^0.8.3;
    error NotEnough();
    contract GasSaver {
       uint256 public number;
       //Gas Used: 21898
        function setNumber(uint256 _number) public {
            require(_number > 10, "number too small please");
            number = _number;
        }
         //Gas Used: 21669
         function setNumberAndRevert(uint256 _number) public {
            if ( _number < 10 ){
                revert NotEnough();
            }
            number = _number;
        }
    }
    

    메모리에 너무 많은 데이터를 로드하지 마십시오.

    Loading data in memory in a single transaction has the effect of increasing gas used. The gas used increase in a quadratic factor when more than 32kb of memory is used in a single transaction. Let's see an example.

    //SPDX-License-Identifier:MIT;
    pragma solidity ^0.8.3;
    contract One {
        //Gas used: 29261
        function setArrayInMemory() public pure {
            uint256[1000] memory _array;
        }
    }
    
    contract Two {
        //Gas used: 276761
        function setArrayInMemory() public pure {
            uint256[10000] memory _array;
        }
    }
    
    //Gas used: 922855
    contract Three {
        function setArrayInMemory() public pure {
            uint256[20000] memory _array;
        }
    }
    
    contract Four {
        //Gas used: 3386918
        function setArrayInMemory() public pure {
            uint256[40000] memory _array;
        }
    }
    

    The above example defined four contracts and each contract has a function named setArrayInMemory where memory space is reserved for an array of uint256 .

    Contract One loads reserves space in memory for 1000 uint256 numbers and when the function is executed it uses up 29261 gas . Contract Two creates memory space for 10000 uint256 numbers and we can see that the gas used is 276761 which is roughly about 10 times of the gas used by Contract One .

    Things gets interesting in Contract Three which reserves space for 20000 uint256 numbers. From the gas used which is 922855, we can see that once the memory used in a single transaction exceeds 32 kilobyte, the quadratic factor of the memory expansion cost kicks in thereby increasing the gas used in a quadratic way.

    Do not populate arrays with enormous amounts of items in a single transaction to prevent gas used increase in a quadratic way.

    기타 팁

    • Always put your require statement on top in the function before making any state change so that if the require statement fails, the remaining gas is refunded to the user on revert.

    • When comparing operation using && or || with a require statement; you should put the cheaper part of the operation first so that if it fails, the compiler will not compare the second part thereby saving on gas used.

    In conclusion We can use a combination of these techniques highlighted here to improve the gas consumption of our smart contracts thereby saving our users from spending too much on gas when interacting with our smart contracts.

    Thanks for reading.

    좋은 웹페이지 즐겨찾기