0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Solidity】for文のGas量節約

Posted at

はじめに(Introduction)

Solidityをレビューしてもらったところ for文 を工夫すれば Gas量の節約 になるというアドバイスを2点受けました。
アドバイスどおりにすればGas量の節約になるのか実験してみたいと思います。

アドバイス(Advice)

元のコードは以下です。
一般的な記述方法だと思います。
※:コンパイル後のコードを読んでいるわけではないので以降のコメントはあくまで推測です。

        for (uint256 i = 0; i < array.length; i++) {
            // 処理
        }

継続条件を変数にする

1つめのアドバイスは、for文の真ん中にある ループの継続条件 に使用しているarray.lengthを事前に変数とするという方法です。
配列オブジェクトとしては長さを持っているはずなので元のコードだと、配列オブジェクト⇒配列長をいう参照になると思います。
変数を直接参照する方が手順を1つ減らせてるのでGas量の節約になるような気がします。

        uint256 arrayLength = array.length;
        for (uint256 i = 0; i < arrayLength; i++) {
            // 処理
        }

カウンタ更新を外だしにしてuncheckedで囲む

2つめのアドバイスは、カウンタ変数の更新 を外だしにして uncheckedで囲む という方法です。

        for (uint256 i = 0; i < array.length; ) {
            // 処理
            unchecked {
                i++;
            }
        }

uncheckedとは以下の効果を持つみたいです。

以下の演算子は、オーバーフローまたはアンダーフロー時にアサーションで失敗します、チェックされていないブロック内で使用された場合、エラーなしでラップされます:

++, --, +, binary -, unary -, *, /, %, **
+=, -=, *=, /=, %=

++なのでオーバーフローのアサーション処理をスキップすることでGas量の節約をしているようです。

実験

実際にソースコードを作成して計測してみます。
実験にはHardhatを使用します。
Hardhatのローカルノードに対し実際にコントラクトを実行してGas量を計測します。

実装

コントラクトを作成します。
後の都合上Solidityのバージョンを0.8.22にします。
setAddrs1はアドバイス前のコード
setAddrs2ループの継続条件を変数 にしたもの
setAddrs3i++unchecked にしたもの
setAddrs4は両方を適用したものとなっています。

contracts/Test.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

contract Test {
    mapping(address => bool) private _addrmap;

    function setAddrs1(address[] memory addrs_) external {
        for (uint256 i = 0; i < addrs_.length; i++) {
            _addrmap[addrs_[i]] = true;
        }
    }

    function setAddrs2(address[] memory addrs_) external {
        uint256 addrLength = addrs_.length;
        for (uint256 i = 0; i < addrLength; i++) {
            _addrmap[addrs_[i]] = true;
        }
    }

    function setAddrs3(address[] memory addrs_) external {
        for (uint256 i = 0; i < addrs_.length; ) {
            _addrmap[addrs_[i]] = true;
            unchecked {
                i++;
            }
        }
    }

    function setAddrs4(address[] memory addrs_) external {
        uint256 addrLength = addrs_.length;
        for (uint256 i = 0; i < addrLength; ) {
            _addrmap[addrs_[i]] = true;
            unchecked {
                i++;
            }
        }
    }

    function getAddr(address addr_) external view returns (bool) {
        return _addrmap[addr_];
    }
}

hardhat.config.jsも変更します。Solidityのバージョンを0.8.22に変更します。

hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.22",
  // solidity: "0.8.27",
};

計測するプログラムを以下のようにします。
100個のランダムなアドレスを登録します、これを100回繰り返します。
Gas量はトランザクションのレシートからgasUsedを取得します。

scripts/test.js
const { ethers, ignition } = require("hardhat");
const TestModule = require("../ignition/modules/Test");

async function main() {
    console.log('>>>');
    const { test } = await ignition.deploy(TestModule);
    console.log(test.target);
    const TEST_COUNT = 100;
    const ADDR_COUNT = 100;
    for (let cnt = 0; cnt < TEST_COUNT; cnt++) {
        const wallet = ethers.HDNodeWallet.createRandom();
        let addrs1 = [];
        let addrs2 = [];
        let addrs3 = [];
        let addrs4 = [];
        for (let i = 0; i < ADDR_COUNT; i++) {
            const newWallet = wallet.deriveChild(i);
            addrs1.push(newWallet.deriveChild(1).address);
            addrs2.push(newWallet.deriveChild(2).address);
            addrs3.push(newWallet.deriveChild(3).address);
            addrs4.push(newWallet.deriveChild(4).address);
        }
        const tx1 = await test.setAddrs1(addrs1);
        const receipt1 = await tx1.wait();
        const tx2 = await test.setAddrs2(addrs2);
        const receipt2 = await tx2.wait();
        const tx3 = await test.setAddrs3(addrs3);
        const receipt3 = await tx3.wait();
        const tx4 = await test.setAddrs4(addrs4);
        const receipt4 = await tx4.wait();
        console.log(`${cnt},${receipt1.gasUsed},${receipt2.gasUsed},${receipt3.gasUsed},${receipt4.gasUsed}`);
    }
    console.log('<<<');
}

main().then(() => {
    process.exit(0);
}).catch((error) => {
    console.error(error);
    process.exit(1);
});

計測

ターミナルからHardhatのローカルノードを起動します。

npx hardhat node

別のターミナルを開き、以下のコマンドを実行します。

npx hardhat compile --force
npx hardhat --network localhost  ignition deploy .\ignition\modules\Test.js --reset
npx hardhat --network localhost run .\scripts\test.js

計測の結果

結果をまとめてみました。
setAddrs1100%とした時のsetAddrs2setAddrs3setAddrs4の変化率を求めてみました。

setAddrs1 setAddrs2 setAddrs3 setAddrs4
平均Gas量 2323200.64 2322871.2 2323178.28 2322955.72
変化率 100.000% 99.986% 99.999% 99.989%

ループの継続条件を変数 に関しては効果がありそうです、といっても$10^{-3}$くらいの影響です。
i++unchecked に関しては影響はなさそうです。

再実験

i++unchecked に関しては影響がでなかったのでいろいろ調べてみたところ、Solidityバージョン0.8.22のリースノートに以下が記載されていました。

コード ジェネレーター: カウンター変数がオーバーフローできない場合、特定の for ループの冗長なオーバーフロー チェックを削除します。

ということは、Solidityバージョン0.8.21では結果が違うはずなので再計測してみます。

コード修正

コントラクトのSolidityバージョンを0.8.21に変更します。

contracts/Test.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.21;

contract Test {
    mapping(address => bool) private _addrmap;

// 以下略

hardhat.config.jsも変更します。Solidityのバージョンを0.8.21に変更します。

hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.21",
  // solidity: "0.8.27",
};

再計測

別のターミナルを開き、以下のコマンドを実行します。

npx hardhat compile --force
npx hardhat --network localhost  ignition deploy .\ignition\modules\Test.js --reset
npx hardhat --network localhost run .\scripts\test.js

再計測の結果

結果をまとめてみました。
以前と同じように、setAddrs1100%とした時のsetAddrs2setAddrs3setAddrs4の変化率を求めてみました。

setAddrs1 setAddrs2 setAddrs3 setAddrs4
平均Gas量 2335204.84 2334868.08 2323176.36 2322961
変化率 100.000% 99.986% 99.485% 99.476%

ループの継続条件を変数 に関しては前回と同じような結果です。
i++unchecked に関しては前回と異なり影響が出ています。

前回の setAddr1 の平均Gas量は2323200.64なので再計測の setAddrs3 と近い値となります。
同じような効果をコンパイラーが行ってると考えられます。

まとめ(Conclusion)

アドバイスは正しかったといえます。
ただ、最新のSolidityをつかってコンパイルするとアドバイスの1つは対処済みとなるようです。
最新版のコンパイラーを利用した方が恩恵を受けるのかもしれません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?