イーサリアムメインネットのGas料金は常に深刻な問題であり、特にネットワークが混雑しているときに顕著です。ピーク時には、ユーザーは非常に高い取引手数料を支払う必要があります。したがって、スマートコントラクトの開発段階でGas料金を最適化することが特に重要です。Gas消費の最適化は、取引コストを効果的に削減するだけでなく、取引効率を向上させ、ユーザーにより経済的で効率的なブロックチェーンの使用体験を提供します。
この記事では、イーサリアム仮想マシン(EVM)のGas料金メカニズム、Gas料金最適化に関する主要な概念、およびスマートコントラクト開発時のGas料金最適化のベストプラクティスについて概説します。これらの内容を通じて、開発者にインスピレーションと実用的な助けを提供し、一般ユーザーがEVMのGas料金の運用方法をより良く理解できるようにし、ブロックチェーンエコシステムの課題に共同で取り組むことを期待しています。
EVMのGas料金メカニズムの概要
EVM互換ネットワークでは、「Gas」は特定の操作を実行するために必要な計算能力を測定する単位を指します。
下の図はEVMの構造レイアウトを示しています。図の中で、Gas消費は3つの部分に分かれています:操作実行、外部メッセージ呼び出し、およびメモリとストレージの読み書き。
出典:イーサリアム公式サイト[1]
各取引の実行には計算リソースが必要であるため、無限ループやサービス拒否(DoS)攻撃を防ぐために一定の費用が徴収されます。取引を完了するために必要な費用は「Gas 料金」と呼ばれます。
自EIP-1559(ロンドンハードフォーク)が有効になって以来、Gas 料金は次の式で計算されます:
Gas fee = 使用されたガスの単位数 * (基本料金 + 優先料金)
基本料金は消失し、優先料金は報酬として、検証者が取引をブロックチェーンに追加することを奨励します。取引を送信する際により高い優先料金を設定すると、次のブロックに取引が含まれる可能性が高まります。これは、ユーザーが検証者に支払う「チップ」のようなものです。
1. EVMにおけるGas最適化を理解する
Solidityでスマートコントラクトをコンパイルすると、契約は一連の「オペコード」、つまりopcodesに変換されます。
あらゆるオペコード(例えば、契約の作成、メッセージ呼び出し、アカウントストレージへのアクセス、および仮想マシンでの操作の実行)には公認のGas消費コストがあり、これらのコストはイーサリアム黄皮書[2]に記録されています。
複数回のEIPの修正を経て、一部のオペコードのGasコストが調整されており、黄皮書の内容と異なる可能性があります。オペコードの最新のコストに関する詳細情報は、こちらを参照してください[3]。
2. Gas最適化の基本概念
Gas最適化の核心理念は、EVMブロックチェーン上でコスト効率の高い操作を優先し、高コストの操作を避けることです。
EVMでは、以下の操作はコストが低いです:
メモリ変数の読み書き
定数および不変変数の読み取り
ローカル変数の読み書き
calldata変数の読み取り、例えばcalldata配列や構造体
内部関数呼び出し
コストの高い操作には以下が含まれます:
契約ストレージ内の状態変数の読み書き
外部関数呼び出し
ループ操作
EVM Gas 料金最適化のベストプラクティス
上記の基本概念に基づいて、開発者コミュニティのためにGas料金最適化のベストプラクティスのリストを整理しました。これらの実践に従うことで、開発者はスマートコントラクトのGas料金消費を削減し、取引コストを低下させ、より効率的でユーザーフレンドリーなアプリケーションを構築できます。
1. ストレージの使用をできるだけ減らす
Solidityにおいて、Storage(ストレージ)は限られた資源であり、そのGas消費はMemory(メモリ)よりもはるかに高くなります。スマートコントラクトがストレージからデータを読み書きするたびに、高額なGasコストが発生します。
イーサリアム黄皮書の定義によれば、ストレージ操作のコストはメモリ操作の100倍以上です。例えば、OPcodesmloadおよびmstore命令はわずか3ガス単位を消費しますが、ストレージ操作であるsloadおよびsstoreは最も理想的な場合でも、コストは少なくとも100単位かかります。
ストレージ使用を制限する方法には以下が含まれます:
非永続データをメモリに格納する
ストレージの変更回数を減らす:中間結果をメモリに保存し、すべての計算が完了した後に結果をストレージ変数に割り当てます。
2. 変数パッケージング
スマートコントラクトで使用されるストレージスロットの数と、開発者がデータを表現する方法は、Gas料金の消費に大きな影響を与えます。
Solidityコンパイラは、コンパイル時に連続するストレージ変数をパッケージ化し、32バイトのストレージスロットを変数ストレージの基本単位として使用します。変数パッケージングは、複数の変数が単一のストレージスロットに適合するように合理的に配置することを指します。
左側は効率の悪い実装方法で、3つのストレージスロットを消費します。右側はより効率的な実装方法です。
この詳細調整により、開発者は20,000ガス単位を節約できます(未使用のストレージスロットを保存するには20,000ガスが必要です)が、現在はわずか2つのストレージスロットで済みます。
各ストレージスロットがGasを消費するため、変数のパッケージングは必要なストレージスロットの数を減らすことによってGasの使用を最適化します。
3. データ型を最適化する
変数は複数のデータ型で表現できますが、異なるデータ型に対応する操作コストは異なります。適切なデータ型を選択することで、Gasの使用を最適化することができます。
例えば、Solidityでは整数を異なるサイズに細分化できます:uint8、uint16、uint32など。EVMは256ビット単位で操作を実行するため、uint8を使用すると、EVMはそれをuint256に変換する必要があり、この変換には追加のGasが消費されます。
図のコードを用いてuint8とuint256のGasコストを比較できます。UseUint()関数は120,382 Gas単位を消費し、UseUInt8()関数は166,111 Gas単位を消費します。
単独で見ると、ここではuint256を使用する方がuint8よりも安価です。しかし、以前に提案した変数パッケージングの最適化を使用する場合は異なります。開発者が4つのuint8変数を1つのストレージスロットにパッケージ化できる場合、それらを反復処理する総コストは4つのuint256変数よりも低くなります。このように、スマートコントラクトは1回のストレージスロットの読み書きを行い、1回の操作で4つのuint8変数をメモリ/ストレージに配置できます。
4. 固定サイズの変数を動的変数の代わりに使用する
データが32バイト以内に収まる場合は、bytesまたはstringsの代わりにbytes32データ型を使用することをお勧めします。一般的に、固定サイズの変数は可変サイズの変数よりもGasを少なく消費します。バイト長を制限できる場合は、bytes1からbytes32の最小長さを選択するようにしてください。
5. マッピングと配列
Solidityのデータリストは、配列(Arrays)とマッピング(Mappings)の2種類のデータ型で表現できますが、構文と構造は大きく異なります。
マッピングはほとんどの場合、効率が高くコストが低いですが、配列は反復可能であり、データ型のパッケージングをサポートします。したがって、データリストを管理する際は、反復が必要ない限り、マッピングを優先して使用することをお勧めします。
6. memoryの代わりにcalldataを使用する
関数パラメータで宣言された変数は、calldataまたはmemoryに保存できます。両者の主な違いは、memoryは関数によって変更される可能性があるのに対し、calldataは不変であることです。
この原則を覚えておいてください:関数のパラメータが読み取り専用である場合、memoryではなくcalldataを優先して使用するべきです。これにより、関数のcalldataからmemoryへの不必要なコピー操作を避けることができます。
例1:メモリを使用する
memoryキーワードを使用する場合、配列の値はABIデコードプロセス中にエンコードされたcalldataからmemoryにコピーされます。このコードブロックの実行コストは3,694ガス単位です。
例2:calldataを使用する
calldataから直接値を読み取るとき、中間のメモリ操作をスキップします。この最適化により、実行コストはわずか2,413ガス単位に抑えられ、Gas効率が35%向上しました。
7. 可能な限り Constant/Immutable キーワードを使用する
Constant/Immutable変数は、契約のストレージに保存されません。これらの変数はコンパイル時に計算され、契約のバイトコードに保存されます。したがって、ストレージと比較してアクセスコストははるかに低く、可能な限りConstantまたはImmutableキーワードを使用することをお勧めします。
8. オーバーフロー/アンダーフローが発生しないことを確認してuncheckedを使用する
開発者が算術操作がオーバーフローやアンダーフローを引き起こさないと確信できる場合、Solidity v0.8.0で導入されたuncheckedキーワードを使用することで、余分なオーバーフローやアンダーフローのチェックを避け、Gas コストを節約できます。
下の図では、条件制約i
さらに、バージョン0.8.0以降のコンパイラではSafeMathライブラリを使用する必要がなくなりました。コンパイラ自体にオーバーフローおよびアンダーフロー保護機能が組み込まれています。
9. 修飾子の最適化
修飾子のコードは修正された関数に埋め込まれ、修飾子を使用するたびにそのコードがコピーされます。これにより、バイトコードのサイズが増加し、Gas 消費が増加します。以下は修飾子のGasコストを最適化する方法の一例です:
最適化前:
最適化後:
この例では、ロジックを内部関数_checkOwner()に再構成することで、修飾子内でその内部関数を再利用できるようにし、バイトコードのサイズを削減し、Gasコストを低下させることができます。
10. 短絡最適化
||および&&演算子について、論理演算は短絡評価が行われます。つまり、最初の条件が論理式の結果を決定できる場合、2番目の条件は評価されません。
Gas 消費を最適化するためには、計算コストが低い条件を前に置くべきです。これにより、高コストの計算をスキップする可能性があります。
一般的な追加のアドバイス
1. 無駄なコードを削除する
契約に未使用の関数や変数がある場合は、それを削除することをお勧めします。これは、契約のデプロイコストを削減し、契約のサイズを小さく保つ最も直接的な方法です。
以下は幾つかの実用的な提案です:
最も効率的なアルゴリズムを使用して計算します。契約内で特定の計算結果を直接使用する場合、冗長な計算プロセスを排除すべきです。本質的に、未使用の計算は削除されるべきです。
イーサリアムでは、開発者はストレージスペースを解放することでGas報酬を得ることができます。変数がもはや必要ない場合は、deleteキーワードを使用して削除するか、デフォルト値に設定するべきです。
ループの最適化:高コストのループ操作を避け、できるだけループを統合し、繰り返し計算をループ本体の外に移動します。
2. プレコンパイルされた契約を使用する
プレコンパイル契約は、暗号化やハッシュ操作などの複雑なライブラリ関数を提供します。コードはEVM上で実行されるのではなく、クライアントノード上でローカルに実行されるため、必要なGasは少なくなります。プレコンパイル契約を使用することで、スマートコントラクトの実行に必要な計算作業を削減し、Gasを節約できます。
プレコンパイル契約の例には、楕円曲線デジタル署名アルゴリズム(ECDSA)やSHA2-256ハッシュアルゴリズムが含まれます。これらのプレコンパイル契約をスマートコントラクトで使用することで、開発者はGasコストを削減し、アプリケーションの実行効率を向上させることができます。
イーサリアムネットワークがサポートするプレコンパイル契約の完全なリストについては、こちらを参照してください[4]。
3. インラインアセンブリコードを使用する
インラインアセンブリ(in-line assembly)を使用すると、開発者はEVMによって直接実行される低レベルで効率的なコードを書くことができ、高価なSolidityオペコードを使用する必要がありません。インラインアセンブリはまた、メモリおよびストレージの使用をより正確に制御できるため、Gas 料金をさらに削減できます。さらに、インラインアセンブリは、Solidityだけでは達成困難な複雑な操作を実行することができ、Gas 消費を最適化するための柔軟性を提供します。
以下はインラインアセンブリを使用してGasを節約するコードの例です:
上の図から、標準的な使用例と比較して、インラインアセンブリ技術を使用した第2の使用例がより高いGas効率を持つことがわかります。
ただし、インラインアセンブリを使用することはリスクを伴い、エラーを引き起こしやすい場合があります。したがって、経験豊富な開発者のみが慎重に使用するべきです。
4. Layer 2 ソリューションを使用する
Layer 2ソリューションを使用すると、イーサリアムメインネット上でストレージおよび計算に必要なデータ量を減少させることができます。
ロールアップ、サイドチェーン、ステートチャネルなどのLayer 2ソリューションは、取引処理をメインイーサリアムチェーンからオフロードすることで、より迅速で安価な取引を実現します。
取引を大量にバンドルすることで、これらのソリューションはチェーン上の取引数を減少させ、Gas料金を削減します。Layer 2ソリューションを使用することで、イーサリアムのスケーラビリティを向上させ、より多くのユーザーやアプリケーションがネットワークに参加できるようになり、ネットワークの過負荷による混雑を防ぐことができます。
5. 最適化ツールとライブラリを使用する
複数の最適化ツールが利用可能で、例えばsolcオプティマイザ、Truffleのビルドオプティマイザ、RemixのSolidityコンパイラなどがあります。
これらのツールは、バイトコードのサイズを最小化し、無駄なコードを削除し、スマートコントラクトの実行に必要な操作の回数を減らすのに役立ちます。他のGas最適化ライブラリ(例えば「solmate」)と組み合わせることで、開発者はGasコストを効果的に削減し、スマートコントラクトの効率を向上させることができます。
結論
Gas消費を最適化することは、開発者にとって重要なステップであり、取引コストを最小化し、EVM互換ネットワーク上のスマートコントラクトの効率を向上させることができます。コスト節約の操作を優先して実行し、ストレージの使用を減らし、インラインアセンブリを利用し、この記事で議論した他のベストプラクティスに従うことで、開発者は契約のGas消費を効果的に削減できます。
ただし、最適化の過程で開発者は慎重に操作し、安全性の脆弱性を引き起こさないようにする必要があります。最適化コードとGas消費を削減するプロセスでは、スマートコントラクトの固有の安全性を決して犠牲にするべきではありません。
[1] :https://ethereum.org/en/developers/docs/gas/
[2] :https://ethereum.github.io/yellowpaper/paper.pdf
[3] :https://www.evm.codes/
[4] :https://www.evm.codes/precompiled