はじめに(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
は ループの継続条件を変数 にしたもの
setAddrs3
は i++
をunchecked
にしたもの
setAddrs4
は両方を適用したものとなっています。
// 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
に変更します。
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.22",
// solidity: "0.8.27",
};
計測するプログラムを以下のようにします。
100個のランダムなアドレスを登録します、これを100回繰り返します。
Gas量はトランザクションのレシートからgasUsed
を取得します。
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
計測の結果
結果をまとめてみました。
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
ループの冗長なオーバーフロー チェックを削除します。
ということは、Solidityバージョン0.8.21
では結果が違うはずなので再計測してみます。
コード修正
コントラクトのSolidityバージョンを0.8.21
に変更します。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.21;
contract Test {
mapping(address => bool) private _addrmap;
// 以下略
hardhat.config.js
も変更します。Solidityのバージョンを0.8.21
に変更します。
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
再計測の結果
結果をまとめてみました。
以前と同じように、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をつかってコンパイルするとアドバイスの1つは対処済みとなるようです。
最新版のコンパイラーを利用した方が恩恵を受けるのかもしれません。