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とすることで打ち消せる。以下のフラグは、示されていない限りデフォルトで無効である。???には小さくまとまった一覧がある。

-favoid-vect

Data Parallel Haskell (DPH)の一部。

デフォルトで無効。ベクトル化回避の最適化を有効にする。この最適化は、-fvectorise変換と組み合わせた場合にのみ動作する。

DPHを使ったコードのベクトル化は大きな改善をもたらすことが多いが、ある種のコードについては結果を悪化させることがある。この最適化は、ベクトル化変換に変更を加えて、関数をベクトル化しない方が良いかどうか判断しようとし、もしそうならそうする。

-fcase-merge

デフォルトで有効。 直接入れ子になったcase式の検査対象が同じ変数である場合、一つにまとめる。例

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

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

-fdicts-cheap

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

-fdo-lambda-eta-expansion

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

-fdo-eta-reduction

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

-feager-blackholing

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

-fexcess-precision:

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

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

-fexpose-all-unfoldings

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

-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は常に完全遅延を適用するとは限らないという違いがあるので、これに頼らないこと。

-ffun-to-thunk

worker-wrapperは使われていない引数を削除するが、通常全ての引数を削除することはない。そうすると、関数クロージャをサンクへと変化させることになり、スペースリークを発生させたり、インライン化を阻害することになるかもしれないからである。このフラグは、worker/wrapperによって値ラムダを全て削除することを許す。デフォルトで無効。

-fignore-asserts:

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

-fignore-interface-pragmas

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

-flate-dmd-anal

デフォルトで無効。単純化パイプラインの最後に、要求解析(demand analysis)をもう一度走らせる。以前には見えなかった正確性を発見する機会を我々はいくつか見つけた。また、-spec-constrなどの最適化が作った関数の未使用の引数を、この後段の要求解析が排除できることがある。改善はささやかであるが、コストもささやかである。Tracのwikiページにあるメモを参照。

-fliberate-case

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

-fliberate-case-threshold=N

liberate-case変換における大きさの閾値を設定する。

-fmax-relevant-bindings=N

型検査器がエラーメッセージの中で型環境の断片を表示することがあるが、このフラグがその最大数を定める。デフォルトは6である。-fno-max-relevant-bindingsによってこれを無効にすると無制限になる。構文的に最上位である束縛は通常除外される(通常たくさんあるので)が、-fno-max-relevant-bindingsはそれらも含める。

-fno-state-hack

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

-fomit-interface-pragmas

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

-fomit-yields

デフォルトで有効。メモリ確保がされない場合、GHCがヒープ検査を省略するようにする。これはバイナリの大きさを5%ほど改善するが、スレッドがメモリ確保のないループを実行している場合、すぐには割り込まれないということも意味する。このようなスレッドに常に割り込めることが重要な場合、この最適化を無効にするべきである。また、割り込み可能性を保証したい場合、この最適化を無効にした上で全てのライブラリを再コンパイルすることを検討するべきである。

-fpedantic-bottoms

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

-fregs-graph

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

-fregs-iterative

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

-fsimpl-tick-factor=n

GHCの最適化器は、停止しない書き換え規則(7.21. 書き換え規則 )を書いたとき、または(もうすこし嫌なことに)データ型を通して再帰を組み上げた場合(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の違いは、これが関数がインライン化されるかどうかを呼び出し地点で決定するのに対し、他方は将来のインライン化のために関数定義を持っておくかどうかを決定することである。

-fvectorise

Data Parallel Haskell (DPH)の一部。

デフォルトで無効。ベクトル化という最適化変換を有効にする。この最適化は、lDPHを使ったプログラムのネストされたデータ並列性を平坦なデータ並列性へと変換する。平坦なデータ並列プログラムはより良い負荷分散を持ち、SIMD並列性を可能にし、キャッシュに関してより友好的な振る舞いが可能になる。

-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

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

ライブラリ作者は、呼び出しパータンへの特殊化を極めて攻撃的に行なうようにGHCに指示することもできる。これはある種の高度に最適化されたライブラリに必要である。そこでは、特殊板の数やコードサイズがどうなろうとも特殊化したいと思うことがあるかもしれない。例として、vectorライブラリでの利用例を単純化したものを挙げる。

import GHC.Types (SPEC(..))

foldl :: (a -> b -> a) -> a -> Stream b -> a
{-# INLINE foldl #-}
foldl f z (Stream step s _) = foldl_loop SPEC z s
  where
    foldl_loop !sPEC z s = case step s of
                            Yield x s' -> foldl_loop sPEC (f z x) s'
                            Skip       -> foldl_loop sPEC z s'
                            Done       -> z

ここで、ループ本体の引数としてSPECがあるので、foldlの本体が呼び出し地点にインライン化された後、foldl_loopに対してGHCは非常に攻撃的な呼び出しパターン特殊化を行なう。GHC.Typesで定義されたSPECはコンパイラによって特別に認識される。

(注意: SPEC引数に対してseqまたはびっくりパターンを使うことが非常に重要である!)

特筆すべき点として、インライン化によって、ループ本体からfが直接見えるようになるので、再帰する選択肢にに関して激しい特殊化が可能になる。

-fspecialise

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

-fstatic-argument-transformation

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

-fstrictness

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

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

-funbox-strict-fields:

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

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

-funbox-small-strict-fields:

デフォルトで有効。このオプションは、正格であると標示(つまり「!」)された構築子フィールドで、表現がポインタ一個以下の大きさであるものを、可能なら全てアンパックする。これは、大きさの制約を満たす全ての正格なフィールドにUNPACKを付けるのと同等である。

例として、以下の構築子を考えよ。

data A = A !Int
data B = B !A
newtype C = C B
data D = D !C

-funbox-small-strict-fieldsが有効だと、これらのフィールドは全て、一つのInt#(7.2. 非ボックス化型とプリミティブ演算を見よ)で表現される。

このオプションは、-funbox-strict-fieldsに比べて大槌を振り回す感が少ない。これが状況を悪化させることは少ないはずである。-funbox-small-strict-fieldsを使ってデフォルトでの非ボックス化を有効にしている場合、NOUNPACKを使って個々の構築子フィールドについてこれを無効にすることができる(7.20.12. NOUNPACKプラグマを見よ)。

整合性のため、32ビットのプラットフォームにおいて、DoubleWord64Int64の構築子フィールドは、技術的にはポインタよりも大きいにも関わらずアンパックされることに注意せよ。