本文へスキップ
このページは英語からの機械翻訳を使用しており、誤りや不明瞭な表現が含まれている可能性があります。最も正確な情報については、オリジナルの英語版をご覧ください。頻繁な更新のため、一部のコンテンツはオリジナルの英語になっている可能性があります。Crowdinでの取り組みに参加して、このページの翻訳改善にご協力ください。 (Crowdin translation page, Contributing guide)

Solidityスマートコントラクトでガス料金を最適化する方法

このガイドでは、Solidityでスマートコントラクトを記述する際のガスコストを最適化する方法について、実践的なステップバイステップのウォークスルーを提供します。

なぜガスの最適化が重要なのか?

ガスの最適化は、スマート・コントラクトの開発において重要な部分である。 ネットワークが混雑しているときでも、スマートコントラクトが効率的でコスト効率に優れていることを保証するのに役立つ。 コントラクト実行の計算オーバーヘッドを削減することで、開発者は取引手数料を下げ、確認時間を短縮し、dApps全体のスケーラビリティを向上させることができる。

開発者にとって、ガスの最適化とは、不必要な計算を最小限に抑え、クリーンで、安全で、予測可能なコードを書くことである。 ユーザーにとっては、過剰な料金を支払うことなく、契約とやり取りできるようにすることだ。

カイアで特に重要な理由

83を超えるMini dAppsがローンチされ、それを数えると、カイア・ブロックチェーンは、これらのオンチェーン・アプリケーションの爆発的な成長によって牽引され、取引量においてEVM互換チェーンをリードする存在となった。

各Mini dAppは、オンチェーンアクションを実行するためにスマートコントラクトに依存している。 アイテムの鋳造、賭け、ゲーム内アセットの管理など、あらゆる契約のやり取りでガスが消費される。 最適化しなければ、これらのdAppsはすぐに、特に規模が大きくなると、ユーザーがやりとりするには高すぎるものになりかねない。

だからこそ、ガス効率は単なるいいとこ取りではないのだ。 それは必要なことだ。 Kaiaで構築する開発者は、機能性とセキュリティを保ちながら、各関数呼び出しがコストを最小化するように最適化されていることを確認しなければならない。

ガス最適化技術

保管梱包

ブロックチェーン上のデータの保存と検索は、特にトランザクションやブロックをまたいでデータを永続させなければならない場合、最もガスコストのかかる作業の一つである。 Solidityでは、このデータは契約ストレージに保存され、永続的でガス代がかかる。 これらのコストを削減するために、開発者はストレージの使用方法、特にステート変数の宣言方法を慎重に最適化しなければならない。

カイア仮想マシン(KVM)は、契約データをストレージスロットと呼ばれる単位に格納する。 各ストレージスロットは、ちょうど256ビット(32バイト)のデータを保持することができる。 例えば、ブールは1バイト、アドレスは20バイトです。

ストレージ・パッキングと呼ばれる技術により、32バイトのストレージ・スロット1つに収まるように、より小さな変数を密に並べることができる。 複数のストレージスロットにアクセスするよりも、1つのストレージスロットから読み出したり、書き込んだりする方が圧倒的に安いからだ。

次の例を考えてみよう:

故障:*。

最適化されていないバージョン (SlotUnOptimized) では、Solidity は構造体を次のように格納します:

  • 宛先アドレス→20バイト必要→スロット0に格納
  • uint256 numConfirmations -> 32バイト必要 -> スロット1に格納
  • uint80の値(10バイト)とboolの実行値(1バイト)→スロット2に格納

value変数とexecuted変数は小さいにもかかわらず、Solidityは明示的に並べ替えを行わない限り、アラインメントパディングにより、それらを独自のストレージスロットに配置します。 その結果、この構造では3つの貯蔵スロットを使用するため、貯蔵運転にかかるガス代は3倍になる。 しかし、address (20 byte) + uint80 (10 byte) + bool (1 byte)の合計サイズは31 byteであり、シングルスロットの32 byte**の制限内に収まる。 小さな変数が一緒にグループ化されるように宣言を並べ替えるだけで、Solidityはそれらを同じスロットにまとめることができます。 これが収納パッキングの真髄だ。

上で見たように、最適化バージョン(*SlotOptimized)では、すべての小さな変数は互いに隣接して配置され、コンパイラーはそれらをより少ないスロットに格納することができます。

キャッシュ・ストレージ

ストレージスロットにどのように変数が配置されているかだけでなく、ストレージへのアクセスや変更に関連するガスコストを理解することも重要だ。

カイア仮想マシンの各ストレージスロットのコストは以下の通り: 初期化(最初の書き込み)に20,000ガス 更新(その後の書き込み)に5,000ガス このため、特に頻繁に呼び出される関数では、ストレージの直接の読み書きの回数を最小限に抑えることが重要です。 効果的なパターンの一つは、関数内で何度もアクセスする必要がある場合に、ストレージ変数をメモリにキャッシュすることである。

次の例を考えてみよう:

変数のデフォルト値への初期化を避ける

Solidityでは、すべてのデータ型に事前に定義されたデフォルト値があります。 例えば、addressのデフォルトはaddress(0)、boolのデフォルトはfalse、uintのデフォルトは0である。 例えば、bool isActive = falseuint total = 0 のように、変数宣言の際に開発者が明示的にデフォルト値を代入すると、よくある非効率が発生する。

これは機能的には正しいのですが、Solidityがデフォルトでこれらの値を設定しているため、デプロイ時に不要なガスコストが発生します。 ステート変数に値を代入せずに宣言することで、コントラクトのバイトコード・サイズを小さくし、余分なストレージ操作を避けることができる。 この小さな調整は、特に複数の変数を扱う場合に、スマート・コントラクトをより効率的で保守しやすくするのに役立つ。

次の例を考えてみよう:

チェーンデータを最小限に

取引で発生するガス代の大半は、契約書保管庫に保存されたデータに由来することを十分に承知している。 実際にどのデータをオンチェーンまたはオフチェーンに保存する必要があるのかを常に問い、両方のオプションのトレードオフを検討するのがベストだ。 完全なオンチェーンNFTの場合、オフチェーンメタデータを使用した従来のNFTと比較して、いかに高価であるかがわかる。 つまり、情報をオフチェーンに保存することで、スマート・コントラクトのガス消費量を大幅に減らすことができる。

未使用ストレージの解放

時々、私たちは契約中の未使用データを解放することを忘れ、必ずガス代が高くなり、ネットワークの肥大化を引き起こす。 いずれにせよ、未使用のストレージの解放は、それがもう使用されないことを確認したら、値を0に戻すだけという簡単なものだ。 また、solidity の特別なキーワード delete を使用して、任意のデータ型を解放することもできます。

次の例を考えてみよう:

特定の関数パラメータについて、メモリではなくcalldataにデータを格納する。

効果的なガス・ゴルフ・テクニックのひとつは、関数の読み取り専用の配列引数にcalldataを使うことだ。 Calldataは、外部呼び出し時に関数の引数が格納される、変更不可能で永続性のない領域である。 ストレージの割り当てやコピーを伴わないため、メモリよりもかなり安価である。

関数が入力配列や文字列を変更せずに読み取るだけでよい場合、パラメータをcalldataとして宣言すると、ガス消費量を減らすことができます。 これは、バッチ転送や複数の受取人のエアドロップのような、頻繁に呼び出されたり、大きな入力データで操作されたりする機能に特に有益である。

次の例を考えてみよう:

配列の代わりにマッピングを使う

Solidity には、データを管理するための 2 つの主要なデータ構造があります:配列 と マッピング** です。 配列は、各要素が特定のインデックスに割り当てられているアイテムのコレクションを格納するもので、順序付きリストに適している。 一方、マッピングはキー・バリュー・ストアとして機能し、一意のキーを通じて値に直接アクセスできる。

配列を扱う場合、特定の値を取り出すにはコレクション全体をループする必要があることが多く、計算ステップごとにガス・コストがかかる。 このため、特に大きなデータセットでは、配列はルックアップの効率が悪くなる。 順序付けられた反復や類似した項目のグループ化が必要でない限り、データのリストを管理するためにマッピングを使用する方がガス効率が良い。

マッピングは定時アクセスを提供し、配列トラバーサルに関連するオーバーヘッドを回避するため、多くのスマート・コントラクトでガス使用を最適化するための好ましい選択となる。

次の例を考えてみよう:

ダイナミック・アレイを超える固定サイズ・アレイ

一般的にマッピングは配列よりもガス効率が良いが、配列が必要な場合もある。 このようなシナリオでは、コンパイル時に要素数がわかっている場合、固定サイズの配列を使うことが望ましい。 固定サイズのアレイは、予測可能なストレージパターンを提供し、サイズ変更操作に伴うオーバーヘッドを回避する。

対照的に、ダイナミックアレイはコントラクトの実行中にサイズが大きくなる可能性があり、メモリ割り当てと境界チェックのために追加のガスコストが発生する。 可能な限り固定サイズのアレイを選択することで、ガスの消費を抑え、スマート・コントラクトの全体的なパフォーマンスを向上させることができる。

この例を見てみよう:

イミュータブルとコンスタントを使用する

Solidityでガスコストを最適化する効果的な方法は、変数を定数または不変として宣言することです。 これらの特殊な変数型は、定数の場合はコンパイル時に、不変量の場合はコントラクトのデプロイ時に、一度だけ値が割り当てられ、その後は読み取り専用となる。 その値はコントラクトのバイトコードに直接埋め込まれるため、スマート・コントラクトの実行で最もコストのかかる操作のひとつであるストレージへのアクセスが不要になる。 このため、コードの明確性と効率を維持しながら、ガス使用量を削減するための強力なツールとなる。

この例を見てみよう:

最適化されたエラー処理

Solidityでガスを最適化する場合、シンプルで効率的に保つことはエラー処理にも当てはまります。 カスタム・エラーは、文字列メッセージを使用する従来のrequire文に代わる省エネルギーを提供する。 文字列ベースのエラー(コントラクト・ バイトコードに格納され、メッセージの長さに応じてサイズが大きくなる)とは異なり、カスタム・エラーはかなり安価だ。

関数セレクタの計算方法と同様に、エラーシグネチャのkeccak256ハッシュから得られるコンパクトな4バイトセレクタを使用することで動作する。 require文やif文の内部で使われるにせよ、カスタム・エラーはバイトコード・サイズとランタイム・ガス・コストを削減するのに役立つ。

この例を見てみよう:

外部可視性修飾子を使用する

ある関数が、外部所有のアカウントや別のスマート・コントラクトによって、コントラクトの外部からのみ呼び出されることが確実な場合、関数の可視性を外部と宣言するのがベスト・プラクティスです。 このガイダンスは、Solidity が関数の引数とメモリ割り当てをどのように処理するかに基づいています。

外部関数は、外部入力用に最適化された読み取り専用セクションであるコールデータから直接パラメータを読み取る。 一方、パブリック関数は内部からも外部からもアクセスできる。 パブリック関数がコントラクトの外から呼び出されると、外部関数と同じように呼び出しデータから引数を読み込む。

しかし、内部的に呼び出される場合、引数はメモリーを介して渡されるため、メモリーの割り当てやコピーが追加され、よりコストがかかる。 特に内部使用目的でない関数を外部とマークすることで、メモリ操作を減らし、ガス効率を向上させることができる。 この小さな変化は、より最適化されたコスト効率の高いスマート・コントラクトにつながる。

この例を見てみよう:

インライン・アセンブリの使用

インライン・アセンブリにより、開発者はKVMのオペコードを直接操作できる。 このテクニックは、特定のシナリオにおいて、標準的なSolidityコードと比較して、よりガス効率の高い実行をもたらします。 これはSolidityの安全チェックをバイパスし、可読性を低下させますが、直接メモリアクセス、ビット演算、カスタム制御フローなどの重要な操作のパフォーマンスには特に有用です。 インライン・アッセンブリを注意深く使用すれば、ボンネットの中でどのようにオペレーションが実行されるかを正確にコントロールできるため、ガス・コストを削減できる。

この例を見てみよう:

結論

ガスコストを最適化することは、Solidityで効率的でコスト効果の高いスマートコントラクトを記述するために不可欠な要素です。 Kaiaで展開することで、ユーザーにはすでに低いトランザクション・コストが提供されているが、実証済みのガス最適化技術を適用することは開発者の責任である。 このガイドに記載されているプラクティスに従うことで、実行コストを大幅に削減し、契約のスケーラビリティを向上させ、ユーザーによりシームレスで持続可能なエクスペリエンスを提供することができます。

ページを改善してください。