Chapter 23

Statement-Scale Simplifications

かるでね
かるでね
2024.03.03に更新

https://docs.soliditylang.org/en/latest/internals/optimizer.html#statement-scale-simplifications

CircularReferencesPruner

CircularReferencesPrunerは、特定の最適化ステップで、相互に呼び出し合う関数群を取り除きますが、これらの関数が外部から参照されていない、または最も外側のコンテキストから参照されていない場合に限ります。
この最適化の目的は、コード内で不必要に循環参照されている関数を削除することにより、コードのサイズを減少させ、実行効率を向上させることです。

作用機序

このステージでは、以下の特徴を持つ関数群が対象になります。

  • 相互参照
    • 関数が互いに直接または間接的に呼び出し合う。
  • 外部参照の欠如
    • これらの関数が外部(つまり、コントラクトの外からの呼び出しや、公開されているインターフェイスを通じて)から参照されていない。
  • 最外コンテキストからの参照の欠如
    • コントラクトの最も外側のレベル(例えば、グローバルスコープや、他の全ての関数からアクセス可能なコンテキスト)からこれらの関数が参照されていない。

効果

  • コードサイズの削減
    • 不要な関数を取り除くことで、コンパイルされたコードのサイズを減少させることができます。
    • これは、特にガスコストを意識する必要があるブロックチェーン環境において重要です。
  • 実行効率の向上
    • 循環参照による潜在的な実行パスの複雑さを排除することで、スマートコントラクトの実行効率が向上します。

適用シナリオ

  • スマートコントラクトやその他のプログラムが大規模で複雑になり、開発過程で不要になった古い関数群が残っている場合。
  • リファクタリングや機能の更新により、一部の関数が実質的に使われなくなったが、コードからまだ削除されていない場合。

CircularReferencesPrunerを適用することで、よりクリーンで効率的なコードベースを維持し、リソースの使用を最適化することができます。

ConditionalSimplifier

ConditionalSimplifierは、制御フローから条件変数の値を決定できる場合に、それらの変数への代入を挿入することで条件文を単純化する最適化ステージです。
この工程は、条件式がゼロではないことをチェックするだけで特定の値を代入することは現在のところ限定的です。
これは、Yul言語においてbool型がサポートされていないため、特定の真偽値(true/false)を直接扱うことができないためです。

現在の機能

  • switchケース
    • <condition> := <caseLabel>という形で、switch文の各ケースに基づいて条件変数への代入を挿入します。
  • if文の後
    • 制御フローが終了するif文の後に<condition> := 0という形で代入を挿入します。
    • これは、if文が真(true)の場合に制御フローがそのブロック内で終了することを意味し、したがってその後の条件は偽(false、すなわち0)であると見なされる場合に使用されます。

将来の機能

  • 1による代替
    • 条件が真の場合に1による代替を許可するようにする予定です。
  • ユーザー定義関数の終了を考慮
    • ユーザー定義関数の制御フローが終了することを考慮に入れる予定です。

前提条件と相性の良いステップ

Disambiguatorが事前に適用されている必要があります。
これにより、各識別子が一意になり、条件の単純化が容易になります。
SSA形式でコードが記述されていると、ConditionalSimplifierはより効果的に動作します。
SSA形式では、各変数が一度だけ割り当てられるため、条件変数の値をより簡単に追跡できます。
不要なコード(デッドコード)の削除が事前に行われていると、条件式の単純化に役立ちます。

このステージは、プログラムの読みやすさを向上させるだけでなく、実行効率の向上にも寄与する可能性がありますが、SSA形式を破壊する可能性があるため、適用するタイミングとその後の最適化ステップには注意が必要です。

ConditionalUnsimplifier

ConditionalSimplifierの逆。

ControlFlowSimplifier

ControlFlowSimplifierは、コントロールフローの構造を単純化することを目的とした最適化ステージです。
このステージは、プログラムの読みやすさと効率を向上させるために、いくつかの一般的なコントロールフローのパターンをよりシンプルな形に変換します。
以下は、ControlFlowSimplifierが行う主な変換の例です。

  • 空の本体を持つif文の置換

    • 条件を評価するだけで何もしないif文は、条件をpopする命令に置き換えられます。
  • 空のデフォルトswitchケースの削除

    • 何もしないデフォルトケースがあるswitch文からは、そのケースが削除されます。
  • デフォルトケースが存在しない場合の空のswitchケースの削除

    • 何もしないケースがあるがデフォルトケースがないswitch文からは、その空のケースが削除されます。
  • ケースがないswitchの置換

    • ケースがまったくないswitch文は、式をポップする命令に置き換えられます。
  • 単一ケースのswitchifに変換

    • 1つのケースのみを持つswitch文は、if文に変換されます。
  • デフォルトケースのみを持つswitchの置換

    • デフォルトケースのみを持ち、その他のケースがないswitch文は、式をpopし、本体をそのまま実行する命令に置き換えられます。
  • 定数式を持つswitchの置換

    • 式が定数で、一致するケースの本体が直接実行されるようにswitch文が置換されます。
  • 他のbreak/continueがなく、制御フローが終了するforifへの置換

    • ループ内でbreakcontinueが使用されておらず、制御フローが終了するforループは、if文に置き換えられます。
  • 関数の末尾のleaveの削除

    • 関数の最後で無駄になるleave命令は削除されます。

これらの最適化はデータフローに依存せず、プログラムの構造のみに基づいています。StructuralSimplifierは、データフローに依存する類似のタスクを実行します。ControlFlowSimplifierは、そのトラバース中にbreakcontinueのステートメントの存在または欠如を記録します。

前提条件

DisambiguatorFunctionHoisterForLoopInitRewriterが適用されている必要があります。
これらのステージにより、識別子が一意になり、関数定義が適切に配置され、forループの初期化がループの外に移動されます。

注意点

現時点ではEVMコードにのみ使用でき、EVMのオペコードを導入するため、他の環境やコンテキストでの使用には適していません。

DeadCodeEliminator

DeadCodeEliminatorは、到達不可能なコード、つまり実行されることがないコードを削除する最適化ステージです。
このステージはプログラムの効率を向上させ、不要なコードによる混乱を減らすことを目的としています。

到達不可能なコードとは?

到達不可能なコードは、以下のいずれかによって先行されるブロック内のコードです。

  • leave
  • return
  • invalid
  • break
  • continue
  • selfdestruct
  • revert
  • 無限に再帰するユーザー定義関数への呼び出し

これらの命令や呼び出しは、それらが現れると以降のコードが実行されることがないため、プログラムの実行フローに影響を与えます。

関数定義の取り扱い

関数定義は、以前のコードから呼び出される可能性があるため、到達可能と見なされ、この最適化ステージによって削除されません。
関数定義はそのまま保持されます。

forループの初期化ブロック

forループの初期化ブロックで宣言された変数は、ループ本体においてもスコープが拡張されます。
このため、DeadCodeEliminatorを実行する前に、ForLoopInitRewriterが実行される必要があります。
これは、初期化ブロック内のコードがループ本体に影響を与える可能性があるためです。

前提条件

DeadCodeEliminatorを実行する前に、以下の最適化ステージが適用されている必要があります。

  • ForLoopInitRewriter
    • forループの初期化部分をループの外に移動させることで、forループの扱いを簡素化します。
  • FunctionHoister
    • 関数定義をトップレベルに移動させることで、関数がより簡単にアクセス可能になります。
  • FunctionGrouper
    • すべての関数定義を単一のブロックにまとめることで、プログラムの構造を整理します。

これらのステージによってプログラムが適切な形式に整えられた後、DeadCodeEliminatorはより効果的に不要なコードを特定し、削除することができます。

EqualStoreEliminator

EqualStoreEliminatorは、特定の条件下で不要なメモリ(mstore)やストレージ(sstore)への書き込みを削除する最適化ステップです。
このステップは、以前に同じキー(k)と値(v)でmstoresstoreが呼び出され、その後に他のストア操作が介在せず、かつkvの値が変化していない場合に、それらの呼び出しを削除します。

この最適化は、以下の理由で特に有効です。

  • **SSA変換(Static Single Assignment)**が適用された後では、変数が一度割り当てられるとその値が変更されないことが保証されます。
    • これにより、kvの値が変わらない限り、連続するmstoresstoreの呼び出しで同じ値が使用されることがわかります。
  • **共通部分式の削除(Common Subexpression Eliminator)**が行われると、既知の同一の値に対して正確に同じ変数が再利用されるようになります。
    • これにより、同じキーと値に対する複数のストア操作が検出されやすくなり、不要なストア操作を効率的に削除できます。

この最適化ステップの前提条件には、DisambiguatorForLoopInitRewriterがあります。これらは、プログラムがこの最適化ステップを適用するための適切な状態にあることを保証します。
Disambiguatorは、同じ名前の変数が異なるものを指していないように識別子の名前を一意にすることで、最適化プロセスを容易にします。
ForLoopInitRewriterは、forループの初期化部分をループの外に移動させることで、後続の最適化ステップがより簡単に適用できるようにします。

このように、EqualStoreEliminatorは、プログラムから冗長なメモリやストレージへの書き込みを削除することで、ガスの使用量を減らし、スマートコントラクトの実行効率を向上させる役割を果たします。

UnusedPruner

UnusedPrunerは、使用されていない関数の定義や変数の宣言をプログラムから削除する最適化ステップです。
このステップは、コード内で参照されることのない関数や変数を特定し、それらを除去することによって、コードのサイズを減らし、実行効率を向上させることを目的としています。

主な機能は以下の通りです。

関数の定義の削除

プログラム内で一度も呼び出されない、または参照されない関数が存在する場合、その関数の定義は不要であると見なされ、削除されます。
これにより、コードベースがスリムになり、コンパイル後のバイトコードのサイズが減少します。

変数宣言の削除

使用されていない変数に関する宣言も同様に削除されます。
ただし、割り当てられた値が「移動可能」でない場合(副作用があるか、環境の状態に依存する表現など)、その表現自体は保持されますが、その値は破棄されます。
これは、副作用を発生させる可能性のある表現を安全に扱うためです。

移動可能な表現文の削除

割り当てられていない(つまり、何らかの変数に値が保存されていない)移動可能な表現は、プログラムの実行に影響を与えないため、削除されます。
これには、副作用のない計算や値の読み取りが含まれますが、その結果がどこにも使用されていない場合です。

この最適化ステップは、プログラムのクリーンアップと効率化に貢献し、特に大規模なプロジェクトや複雑なスマートコントラクトで有効です。
未使用のコードを削除することで、デプロイメントのコストを下げ、実行時のガス消費を削減することが可能になります。

StructuralSimplifier

StructuralSimplifierは、プログラムの構造的なレベルでさまざまな種類の簡素化を行う一般的な最適化ステップです。
このステップは、コードをより効率的かつ読みやすくするために、不要な制御フロー構造を簡略化または削除します。
データフローアナライザーを使用して、これらの変更が安全に行われることを確認します。

主な機能としては以下のような変更があります。

  • 空の本体を持つif文の置換

    • 条件を評価するだけで何も行わないif文は、その条件をpop操作に置き換えることで簡略化されます。
    • これは、条件の評価結果が使用されない場合に余分な操作を削除します。
  • 真の条件を持つif文の置換

    • 常に真と評価される条件を持つif文は、その本体に置き換えられます。
    • これにより、不必要な条件チェックが削除されます。
  • 偽の条件を持つif文の削除

    • 常に偽と評価される条件を持つif文は、完全に削除されます。
    • この場合、if文の本体は決して実行されないため、コードから取り除かれます。
  • 単一ケースのswitchをifに変換

    • ケースが1つだけのswitch文は、より単純なif文に変換されます。
    • これにより、コードの読みやすさが向上します。
  • デフォルトケースのみを持つswitchの置換

    • デフォルトケースのみを持つswitch文は、その表現をpop操作と本体に置き換えることで簡略化されます。
    • これは、switch文が単一の実行パスしか持たない場合に不要な構造を削除します。
  • リテラル式によるswitchの置換

    • 式がリテラル(定数)であるswitch文は、その式に一致するケースの本体に置き換えられます。
    • これにより、実行時に不要な条件分岐を削除できます。
  • 偽の条件を持つforループの置換

    • 常に偽と評価される条件を持つforループは、その初期化部分に置き換えられます。
    • これは、ループの本体が決して実行されない場合に、不要なループ構造を削除します。

これらの簡素化によって、プログラムはより効率的に実行され、読みやすく、保守しやすくなります。
特に大規模なコードベースや複雑な制御フローを持つプログラムにおいて、この種の最適化は非常に有益です。

BlockFlattener

BlockFlattenerは、ネストされたブロックを取り除き、内側のブロックにあるステートメントを外側のブロックの適切な場所に挿入することでコードを単純化する最適化ステージです。
この処理は、特にFunctionGrouperに依存しており、FunctionGrouperによって生成された形式を維持するために最外層のブロックはフラット化しません。

例として、以下のネストされたブロックを持つコードがあります。

{
    {
        let x := 2
        {
            let y := 3
            mstore(x, y)
        }
    }
}

BlockFlattenerを適用した後、このコードは以下のように変換されます。

{
    let x := 2
    let y := 3
    mstore(x, y)
}

この変換により、コードの読みやすさが向上し、コードの構造が単純化されます。
内側のブロックが外側のブロックにマージされることで、スコープの管理が簡素化され、変数のスコープが広がる可能性がありますが、コードが事前に曖昧さを解消されている(つまり、すべての識別子が一意である)場合、これは問題を引き起こしません。

BlockFlattenerは、特に大きなネスト構造を持つプログラムや、コードの可読性を高めたい場合に役立ちます。
また、コードの構造を単純化することで、他の最適化ステップがより効率的に動作する基盤を提供します。

LoopInvariantCodeMotion

LoopInvariantCodeMotionは、プログラムのパフォーマンスを向上させるための最適化手法の一つです。
この最適化は、ループの不変式(ループの実行中に値が変わらない式や変数)をループの外側に移動させることで、ループの繰り返しの中で不必要に繰り返される計算を減らします。
これにより、計算の重複が避けられ、プログラムの実行効率が向上します。

基本的な考え方

ループの中で、そのループのイテレーション間で値が変わらない変数の宣言をループの外に移動させます。
これにより、その変数の計算がループの各イテレーションで繰り返されるのを防ぎます。

要件

DisambiguatorForLoopInitRewriter、および FunctionHoister が前もって実行される必要があります。
これらのステップにより、コードが整理され、変数が明確に区別され、関数が適切に配置されます。
Expression SplitterSSA Transform も前もって実行されることが推奨されます。
これにより、変数の宣言が単純化され、不変式がループの外に移動しやすくなります。

実行の条件

ループの本体または後処理ブロックの最上位レベルにあるステートメントのみが考慮されます。
つまり、条件分岐内の変数宣言はループの外に移動されません。
移動される変数宣言は、SSA(Static Single Assignment)形式である必要があります。
これは、各変数が宣言されてから一度しか値が割り当てられない形式です。

例えば、以下のようなループがあるとします。

for (let i = 0; i < 10; i++) {
    let invariant = computeInvariant();
    // その他のループ内処理...
}

computeInvariant() の結果がループの各イテレーションで変わらない場合、LoopInvariantCodeMotion 最適化によってこの計算がループの外に移動され、以下のようになります。

let invariant = computeInvariant();
for (let i = 0; i < 10; i++) {
    // その他のループ内処理...
}

これにより、computeInvariant() の呼び出しが一度だけになり、ループの実行効率が向上します。