[Solidity] Gas Optimizations, Bit Operators, Unchecked Math

jhcha·2023년 8월 8일
0

Solidity

목록 보기
13/17
post-thumbnail

Gas Optimizations

url: https://solidity-by-example.org/gas-golf/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

// gas golf
contract GasGolf {
    // start - 50908 gas
    // use calldata - 49163 gas
    // load state variables to memory - 48952 gas
    // short circuit - 48634 gas
    // loop increments - 48244 gas
    // cache array length - 48209 gas
    // load array elements to memory - 48047 gas
    // uncheck i overflow/underflow - 47309 gas

    uint public total;

    // start - not gas optimized
    // function sumIfEvenAndLessThan99(uint[] memory nums) external {
    //     for (uint i = 0; i < nums.length; i += 1) {
    //         bool isEven = nums[i] % 2 == 0;
    //         bool isLessThan99 = nums[i] < 99;
    //         if (isEven && isLessThan99) {
    //             total += nums[i];
    //         }
    //     }
    // }

    // gas optimized
    // [1, 2, 3, 4, 5, 100]
    function sumIfEvenAndLessThan99(uint[] calldata nums) external {
        uint _total = total;
        uint len = nums.length;

        for (uint i = 0; i < len; ) {
            uint num = nums[i];
            if (num % 2 == 0 && num < 99) {
                _total += num;
            }
            unchecked {
                ++i;
            }
        }

        total = _total;
    }
}

Solidity에서 가스를 절약하기 위해 다음과 같은 방법을 사용할 수 있다.

  • memory를 calldata로 대체해서 사용
  • 상태 변수를 메모리에 로딩
  • loop에서 사용하는 증감 연산자를 i++ 대신 ++i로 대체해서 사용
  • 배열 요소 캐싱
  • Short circuit (단락)
    • ||, && 연산의 단락 규칙을 통해 연산을 절약할 수 있다.
    • f(x) || g(y) 비용이 적게 드는 기능을 f(x)로 설정, 비용이 많이 드는 기능을 g(x)로 설정
      unchecked 키워드는 아래 예제에서 다룬다.

가스 최적화를 위한 작업 중 코드에 어려운 부분은 없지만, 상태 변수를 메모리에 로딩하여 연산하는게 더 가스 소모량이 적은지만 비교해서 확인했다.

function sumIfEvenAndLessThan99(uint[] calldata nums) external {
        uint len = nums.length;

        for (uint i = 0; i < len; ) {
            uint num = nums[i];
            if (num % 2 == 0 && num < 99) {
                total += num;
            }
            unchecked {
                ++i;
            }
        }
    }

메모리를 사용하는 지역변수 _total을 없애고, 상태변수 total를 직접 변경시켰다.
상태변수 변경 전에 메모리를 사용한 경우

메모리를 사용하지 않고 상태변수 변경을 한 경우

그림에서 보이는 것 처럼, 실제로 가스 소모량이 증가한 것을 확인할 수 있다.

Bitwise Operators

url: https://solidity-by-example.org/bitwise/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract BitwiseOps {
    // x     = 1110 = 8 + 4 + 2 + 0 = 14
    // y     = 1011 = 8 + 0 + 2 + 1 = 11
    // x & y = 1010 = 8 + 0 + 2 + 0 = 10
    function and(uint x, uint y) external pure returns (uint) {
        return x & y;
    }

    // x     = 1100 = 8 + 4 + 0 + 0 = 12
    // y     = 1001 = 8 + 0 + 0 + 1 = 9
    // x | y = 1101 = 8 + 4 + 0 + 1 = 13
    function or(uint x, uint y) external pure returns (uint) {
        return x | y;
    }

    // x     = 1100 = 8 + 4 + 0 + 0 = 12
    // y     = 0101 = 0 + 4 + 0 + 1 = 5
    // x ^ y = 1001 = 8 + 0 + 0 + 1 = 9
    function xor(uint x, uint y) external pure returns (uint) {
        return x ^ y;
    }

    // x  = 00001100 =   0 +  0 +  0 +  0 + 8 + 4 + 0 + 0 = 12
    // ~x = 11110011 = 128 + 64 + 32 + 16 + 0 + 0 + 2 + 1 = 243
    function not(uint8 x) external pure returns (uint8) {
        return ~x;
    }

    // 1 << 0 = 0001 --> 0001 = 1
    // 1 << 1 = 0001 --> 0010 = 2
    // 1 << 2 = 0001 --> 0100 = 4
    // 1 << 3 = 0001 --> 1000 = 8
    // 3 << 2 = 0011 --> 1100 = 12
    function shiftLeft(uint x, uint bits) external pure returns (uint) {
        return x << bits;
    }

    // 8  >> 0 = 1000 --> 1000 = 8
    // 8  >> 1 = 1000 --> 0100 = 4
    // 8  >> 2 = 1000 --> 0010 = 2
    // 8  >> 3 = 1000 --> 0001 = 1
    // 8  >> 4 = 1000 --> 0000 = 0
    // 12 >> 1 = 1100 --> 0110 = 6
    function shiftRight(uint x, uint bits) external pure returns (uint) {
        return x >> bits;
    }

    // Get last n bits from x
    function getLastNBits(uint x, uint n) external pure returns (uint) {
        // Example, last 3 bits
        // x        = 1101 = 13
        // mask     = 0111 = 7
        // x & mask = 0101 = 5
        uint mask = (1 << n) - 1;
        return x & mask;
    }

    // Get last n bits from x using mod operator
    function getLastNBitsUsingMod(uint x, uint n) external pure returns (uint) {
        // 1 << n = 2 ** n
        return x % (1 << n);
    }

    // Get position of most significant bit
    // x = 1100 = 10, most significant bit = 1000, so this function will return 3
    function mostSignificantBit(uint x) external pure returns (uint) {
        uint i = 0;
        while ((x >>= 1) > 0) {
            ++i;
        }
        return i;
    }

    // Get first n bits from x
    // len = length of bits in x = position of most significant bit of x, + 1
    function getFirstNBits(uint x, uint n, uint len) external pure returns (uint) {
        // Example
        // x        = 1110 = 14, n = 2, len = 4
        // mask     = 1100 = 12
        // x & mask = 1100 = 12
        uint mask = ((1 << n) - 1) << (len - n);
        return x & mask;
    }
}

비트 연산은 다른 언어에서 지원하는 비트 연산과 동일하다.
예제에 설명이 잘 되어있어 입력한 값에 대한 연산 결과를 얻을 수 있다.
특히, shiftLeft와 같이 주어진 길이를 넘어서는 오버플로우가 발생했을 때 결과가 궁금하여 아래와 같이 코드를 수정했다.

    function shiftLeft(uint8 x, uint8 bits) external pure returns (uint) {
        return x << bits;
    }

uint는 2^8까지 지원하므로 x에 1, bits>=8 값을 넣어보면 항상 0이 나오는 것을 알 수 있다.

Most significant bit

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract MostSignificantBitFunction {
    // Find most significant bit using binary search
    function mostSignificantBit(uint x) external pure returns (uint msb) {
        // x >= 2 ** 128
        if (x >= 0x100000000000000000000000000000000) {
            x >>= 128;
            msb += 128;
        }
        // x >= 2 ** 64
        if (x >= 0x10000000000000000) {
            x >>= 64;
            msb += 64;
        }
        // x >= 2 ** 32
        if (x >= 0x100000000) {
            x >>= 32;
            msb += 32;
        }
        // x >= 2 ** 16
        if (x >= 0x10000) {
            x >>= 16;
            msb += 16;
        }
        // x >= 2 ** 8
        if (x >= 0x100) {
            x >>= 8;
            msb += 8;
        }
        // x >= 2 ** 4
        if (x >= 0x10) {
            x >>= 4;
            msb += 4;
        }
        // x >= 2 ** 2
        if (x >= 0x4) {
            x >>= 2;
            msb += 2;
        }
        // x >= 2 ** 1
        if (x >= 0x2) msb += 1;
    }
}

if문에 따라 시프트 연산으로 x 값을 변경하고, 그에 대한 msb 상태 값을 누적 연산해서 최상위 비트 위치를 구할 수 있다.

// ex) x = 33(10) = 0010 0001(2), msb = 0;
if (x >= 0x10) {
	x >>= 4;
	msb += 4;
}

// x = 0000 0010(2), msb = 4;
if (x >= 0x2) msb += 1;

// returns msb = 5;

Unchecked Math

url: https://solidity-by-example.org/unchecked-math/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract UncheckedMath {
    function add(uint x, uint y) external pure returns (uint) {
        // 22291 gas
        // return x + y;

        // 22103 gas
        unchecked {
            return x + y;
        }
    }

    function sub(uint x, uint y) external pure returns (uint) {
        // 22329 gas
        // return x - y;

        // 22147 gas
        unchecked {
            return x - y;
        }
    }

    function sumOfCubes(uint x, uint y) external pure returns (uint) {
        // Wrap complex math logic inside unchecked
        unchecked {
            uint x3 = x * x * x;
            uint y3 = y * y * y;

            return x3 + y3;
        }
    }
}

unchecked 키워드는 solidity 0.8.0 이상 버전에서 지원한다. solidity 0.8.0 버전 이후로 solidity는 컴파일 과정에서 자동적으로 오버플로우, 언더플로우에 대한 검사를 지원한다. 그러므로, 오버플로우나 언더플로우에 대한 검사를 위한 연산때문에 추가적으로 가스가 소모된다.
따라서, 오버플로우 언더플로우에 대한 검사를 비활성화하는 키워드가 unchecked이고, 비활성화를 통해 가스를 절약할 수 있다.

0개의 댓글