【Solidity】for文のGas量節約

# はじめに（Introduction）

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

# アドバイス（Advice）

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


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

## 継続条件を変数にする

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

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

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

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


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

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

> 以下の演算子は、オーバーフローまたはアンダーフロー時にアサーションで失敗します、チェックされていないブロック内で使用された場合、エラーなしでラップされます：
> 
> `++`, `--`, `+`, binary `-`, unary `-`, `*`, `/`, `%`, `**`
`+=`, `-=`, `*=`, `/=`, `%=`

https://docs.soliditylang.org/en/v0.8.28/control-structures.html#checked-or-unchecked-arithmetic

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

# 実験

実際にソースコードを作成して計測してみます。
実験には[Hardhat](https://hardhat.org/)を使用します。
Hardhatのローカルノードに対し実際にコントラクトを実行してGas量を計測します。

## 実装

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

```javascript: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`に変更します。

```javascript: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`を取得します。

```javascript: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のローカルノードを起動します。

```powershell
npx hardhat node
```

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

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

## 計測の結果

結果をまとめてみました。
`setAddrs1`を`100%`とした時の`setAddrs2`、`setAddrs3`、`setAddrs4`の変化率を求めてみました。

||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` ループの冗長なオーバーフロー チェックを削除します。

https://github.com/ethereum/solidity/releases/tag/v0.8.22

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

## コード修正

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

```javascript: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`に変更します。

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

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

## 再計測

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

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

## 再計測の結果

結果をまとめてみました。
以前と同じように、`setAddrs1`を`100%`とした時の`setAddrs2`、`setAddrs3`、`setAddrs4`の変化率を求めてみました。

||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をつかってコンパイルするとアドバイスの１つは対処済みとなるようです。
最新版のコンパイラーを利用した方が恩恵を受けるのかもしれません。