4.10. 最適化(コードの改善)

-O*オプションは便利な最適化フラグの「詰め合わせ」を指定するのに使う。後で説明される-f*オプションは個々の最適化を有効/無効にするのに使う。-m*機械固有の最適化を有効/無効にするのに使う。

4.10.1. -O*: 便利な最適化フラグの「詰め合わせ」。

GHCが生成するコードの質に影響を与えるオプションは大量にある。大抵の人には一般的な目標しかない。つまり、「素早くコンパイルすること」であるとか「電光石火のように走るプログラムを生成すること」などである。以下に示す最適化の「詰め合わせ」を指定すれば(あるいは指定しないことを選べば)十分なはずである。

注意点として、高い最適化水準では多くのモジュール間最適化が行われ、何かを変更したときにどの程度再コンパイルが必要かに影響を与える。これは開発中に非最適化を貫くことの理由の一つである。

-O*が指定されないとき:

「なるべく速くコンパイルしてほしい。できたコードの品質についてはうるさくいわない」という意味にとられる。例えば、ghc -c Foo.hsのような場合である。

-O0:

「全ての最適化を無効にせよ」という意味であり、-Oが指定されていないかのような状態に戻す。-O0は、例えばmakeが既に-Oをコマンド行に挿入しているときに便利である。

-Oまたは-O1:

「高品質のコードをそれほど時間を掛けずに生成せよ」という意味である。例えば、ghc -c -O Main.lhsのように使われる。

-O2:

「危険でない全ての最適化を適用せよ。コンパイルに非常に時間が掛かっても構わない」という意味である。

避けられる「危険」な最適化とは、運が悪いときに実行時間・空間を悪化させるおそれのあるものである。通常これらは個々に設定される。

現時点では、-O2-Oよりも良いコードを生成することは考えにくい。

我々は日々の作業では-O*を使わない。それなりの速度が必要なときは-Oを使う。例えば、何かを計測するときなどである。「とにかくハイクオリティがいいお( ^ω^) 時間とか気にしないお!CPU稼働率100%でも構わないお!」というあなたには、-O2オプションをどうぞ。ガリガリ音を立てるPCを後にして、コーヒーを100万回飲みに行こう。(コーヒーブレイクするってレベルじゃねぇぞ!)

-O(など)が何を「実際に意味している」かを知るもっとも簡単な方法は、-vを付けて走らせ、驚きのあまり後ずさることである。

4.10.2. -f*: プラットフォーム非依存のフラグ

これらのフラグは個々の最適化を有効・無効にする。これらは通常、上記の-O系のオプションを介して設定され、したがって、どれも明示的に指定する必要はないはずである。(実際、そうすると予期せぬ結果が訪れるかもしれない)。-fhogeというフラグは-fno-hogeとすることで打ち消せる。以下のフラグは、示されていない限りデフォルトで無効である。

-fcse

デフォルトで有効。共通部分式削除の最適化を有効にする。unsafePerformIO式が複数あって、共通化されたくない場合には、これを切るのが便利なことがある。

-fstrictness

デフォルトで有効。正格性解析器を有効にする。GHCの正格性解析器については非常に古い論文Measuring the effectiveness of a simple strictness analyserがあるが、現在のものとはかなり違いがある。

正格性解析器は、どの引数と変数が関数内で「正格に」(つまり、その関数内でいずれ評価される)使われ得るかを調べる。これによって、lazyな引数に適用された場合にはプログラムの意味を変えるようなある種の最適化(非ボックス化など)をGHCが適用できるようになる。

-funbox-strict-fields:

このオプションは正格と印の付けられた(つまり「!」)構築子フィールドを可能なら全て非ボックス化、つまりアンパックする。これは全ての正格な構築子フィールドにUNPACKプラグマを付けるのと同等である。(7.18.10. UNPACKプラグマを見よ)

このオプションは少々大槌を振り回す感じがある。場合によっては状況を悪化させかねない。UNPACKを使ってフィールドを選択的に非ボックス化する方が良いかもしれない。もう一つの選択肢は、-funbox-strict-fieldsを使ってデフォルトで非ボックス化を有効にしつつ、特定の構築子フィールドについてはNOUNPACKプラグマを使って無効にすることである(7.18.11. NOUNPACKプラグマを見よ)。

-fspec-constr

デフォルトで無効だが、-O2によって有効になる。呼び出しパターンへの特殊化を有効にする。Call-pattern specialisation for Haskell programsを見よ。

この最適化は、再帰関数を、その引数の「形」に対して特殊化する。これは例を出すのが分かり易い。以下を考えよ。

last :: [a] -> a
last [] = error "last"
last (x : []) = x
last (x : xs) = last xs

このコードでは、最初に空リストかどうかの検査をした後では、再帰的に呼ばれた場合にこのパターン照合が冗長であることが分かる。そういうわけで、-fspec-constrは上のコードを次のように変形する。

last :: [a] -> a
last []       = error "last"
last (x : xs) = last' x xs
    where
      last' x []       = x
      last' x (y : ys) = last' y ys

不要なパターン照合を避けるだけでなく、これによって不要なメモリ確保が必要なくなることもある。これは、ある引数が、自己再帰呼び出しの場合には正格だが最初に呼ばれたときにはそうでないという場合にあてはまる。上の例と同様に正格で再帰的な選択肢が作られるからである。

-fspecialise

デフォルトで有効。このモジュールで定義された、型クラスによる多重定義関数それぞれを、このモジュールで使われている型について特殊化する。また、INLINABLEプラグマ(7.18.5.2. INLINABLEプラグマ)を持つインポートされた関数を、このモジュールで呼ばれている型で特殊化する。

-fstatic-argument-transformation

静的引数変換(static argument transformation)を有効にする。これは、再帰的な関数を、再帰的な局所ループを持つ非再帰関数へと変化させるものである。Andre Santosの博士論文の7章を見よ。

-ffloat-in

デフォルトで有効。let束縛を内側、利用位置に近づく方向に移動させる。Let-floating: moving bindings to give faster programs (ICFP'96)を見よ。

この最適化はlet束縛を使用位置に近づける。こうすることの利点は、letの移動先の選択肢が実行されない場合に、不要なメモリ確保を防ぐことができる点である。また、他の最適化過程が局所的により多くの情報を手にすることになるので、より効果的に働くことが可能になる。

しかし、この最適化は常に助けになるわけではない(そのため、GHCはヒューリスティクスを使ってこれを適用すべきか決めている)。詳細は複雑だが、単純な例は、let束縛を外側に動かすことで、複数のlet束縛を単一の大きなlet束縛にまとめることができ、メモリ確保を一度に行なうことでガベッジコレクタとアロケータを助けることができる、という場合である。

-ffull-laziness

デフォルトで有効。完全遅延最適化(let-floatingとも呼ばれる)を走らせる。これは、let束縛を、外側のラムダの外にまで浮動させることで、それが計算される回数が少なくなるように願うものである。Let-floating: moving bindings to give faster programs (ICFP'96)を見よ。完全遅延は共有を促進するが、これはメモリ使用量を増やすことにつながる。

注意: GHCは完全遅延を完全には実装していない。最適化が有効で、-fno-full-lazinessが与えられなかったとき、共有を促進するある種の最適化が実行される。例えば繰り返し実行される計算をループから抽出する、といったことである。これらは完全遅延の実装で行われるのと同じ変換だが、GHCは常に完全遅延を適用するとは限らないという違いがあるので、これに頼らないこと。

-fdo-lambda-eta-expansion

デフォルトで有効。アリティを増やすためにlet束縛をη展開する。

-fdo-eta-reduction

デフォルトで有効。ラムダ式をη簡約することで複数のラムダをまとめて除去できるなら、そうする。

-fcase-merge

デフォルトで有効。直接入れ子になっている二つのcase式が同じ変数を検査しているなら、一つにまとめる。例。

  case x of
     Red -> e1
     _   -> case x of 
              Blue -> e2
              Green -> e3
==>
  case x of
     Red -> e1
     Blue -> e2
     Green -> e2

-fliberate-case

デフォルトで無効だが、-O2によって有効になる。liberate-case変換を有効にする。これは、再帰関数をその右辺に一回展開することで、自由変数が繰り返しcaseで検査されるのを防ぐ。これは呼び出しパターンの特殊化(-fspec-constr)に少し似ているが、引数でなく自由変数を対象にする。

-fdicts-cheap

極めて実験的なフラグ。辞書を値に持つような式のコストを、最適化器が低く見積もるようにする。

-feager-blackholing

GHCは通常スレッドを切り替える場合にのみブラックホール化を行なう。このフラグは、サンクに進入してすぐにこれを行なうようにする。Haskell on a shared-memory multiprocessorを見よ。

-fno-state-hack

State#トークンを持つラムダを、単一進入であり、したがってその中に物をインライン化しても良いとみなす「stateハック」を無効にする。これはIOおよびSTモナドのコードの性能を向上させ得るが、共有を減らす危険を冒している。

-fpedantic-bottoms

GHCがボトムをより精密に扱うようにする(しかし、-fno-state-hackも見よ)。特に、case式を透過してイータ展開をすることがなくなる。このようなイータ展開は性能には良いが、部分適用の結果に対してseqを使っているなら悪いものになる。

-fsimpl-tick-factor=n

GHCの最適化器は、停止しない書き換え規則(7.19. 書き換え規則 )を書いたとき、または(もうすこし嫌なことに)データ型を通して再帰を組み上げた場合(14.2.1. GHCのバグ)に発散する。コンパイラが無限ループに陥るのを避けるため、最適化器は「tickの回数」を保持し、この回数が超過したときにはインライン化と書き換え規則の適用をやめる。大きいプログラムが多くのtickを使えるように、この限界はプログラムの大きさの定数倍になる。-fsimpl-tick-factorはこの定数を変えられるようにする。デフォルトは100である。100より大きな数はより沢山のtickを与え、100より小さい数はより少ないtickを与える。

tickの数が尽きると、GHCはそれまでに実行した単純化器の歩みを要約する。-fddump-simpl-statsを使うとずっと詳細な一覧を生成することができる。通常これでループをかなり精密に同定することができる。いくつかの数がとても大きくなるからである。

-funfolding-creation-threshold=n:

(デフォルト: 45)関数の展開候補(unfolding)に許される最大の大きさを定める。(展開候補には、それが呼び出し点で展開されたときの「コード膨張」のコストを反映した「大きさ」が与えられる。大きい関数ほど大きなコストを持つ)

これによる影響は、(a)これより大きい物は(INLINEプラグマがない限り)決してインライン展開されない (b)これより大きい物は決してインタフェースファイルに吐かれることはない、という点である。

この数値を増やしても、結果としてコードが速くなるというよりは単にコンパイル時間が長くなる公算が高い。-funfolding-use-thresholdの方が便利である。

-funfolding-use-threshold=n:

(デフォルト: 8)これは展開(インライン化)にあたっての魔法のカットオフ値である。これより小さい関数定義は呼び出し元に展開され、これより大きい物は展開されない。関数の大きさは二つのものに依存する。式の実際の大きさと、それに適用される割引である。(-funfolding-con-discountを見よ)

これと-funfolding-creation-thresholdの違いは、これが関数がインライン化されるかどうかを呼び出し地点で決定するのに対し、他方は将来のインライン化のために関数定義を持っておくかどうかを決定することである。

-fexpose-all-unfoldings

実験的なフラグ。非常に大きな関数や再帰関数も含めて、すべての展開を露出する。これによって、全ての関数がインライン化可能になる。ただし、通常GHCは大きい関数をインライン化することを避ける。

-fvectorise

Data Parallel Haskell。

TODO: Document optimisation
-favoid-vect

Data Parallel Haskell。

TODO: Document optimisation
-fregs-graph

デフォルトで無効だが、-O2によって有効になる。ネイティブコード生成器との組み合わせでのみ適用される。ネイティブコード生成器においてグラフ彩色レジスタ割り付け器を使う。デフォルトでは、GHCはもっと単純で速い線形レジスタ割り付け器を使う。欠点は、線形割り付け器は通常、より悪いコードを生成することである。

-fregs-iterative

デフォルトで無効。ネイティブコード生成器との組み合わせでのみ適用される。ネイティブコード生成器において、反復合併グラフ彩色レジスタ割り付け器を使う。これは-freg-graphのものと同じレジスタ割り付け器だが、レジスタ割り付けの間に反復合併(iterative coalescing)を有効にする。

-fexcess-precision:

このオプションが与えられると、中間の浮動小数点数が最終的な型よりも大きな精度/範囲をもつことが許される。一般にはこれは良いことだが、Float/Doubleの正確な精度/範囲に依存したプログラムがあるかもしれず、そのようなプログラムはこのオプションなしでコンパイルせねばならない。

32ビットのx86コード生成器は、excess-precisionモードにのみ対応しているので、-fexcess-precision-fno-excess-precisionも効果を持たない。これは既知のバグであり、14.2.1. GHCのバグを見よ。

-fignore-asserts:

ソースコード中でException.assert関数が使われていても無視する。(言い替えると、Exception.assert p eeに書き換える。7.17. アサーション を見よ)。このフラグは-Oが指定されていると有効になる。

-fignore-interface-pragmas

インタフェースファイルを読むときに必須でない情報を全て無視するようにGHCに指示する。つまり、仮にM.hiに関数の展開候補や正格性情報があったとしても、GHCはその情報を無視する。

-fomit-interface-pragmas

コンパイル中のモジュール(Mとしよう)のインタフェースファイルにおいて、必須でない情報を全て省略するようにGHCに指示する。これによって、Mをインポートするモジュールからは、Mがエクスポートする関数のしか見えず、展開候補や正格性情報などが見えなくなる。よって、例えば、Mからエクスポートされた関数が、それをインポートするモジュールでインライン化されるということがなくなる。これによる利点は、Mをインポートするモジュールを再コンパイルしなければならない頻度が減るということである。(Mのエクスポート物の型が変わった場合だけ再コンパイルすればよく、実装が変わっただけならしなくてよい)