PR

 では,core_todoの定義を見てみましょう。

 最適化オプション-Oで最適化レベルが1以上に設定されている場合,core_todoには「simpl_gentlyで定義された"gently"フェーズ」「simpl_phasesで定義された複数のフェーズ」「フェーズ0」の順に単純化器の実行が並べられていきます。simpl_phasesの「[ simpl_phase phase ["main"] max_iter | phase <- [phases, phases-1 .. 1] ]」というリスト内包表記による定義からわかるように,フェーズの数字は降順に1ずつ少なくなる形になっています。

 つまり,単純化器のフェーズは,phasesの値が2ならば「gently」「2」「1」「0」の順に遷移するよう定義されます。

 フェーズが昇順で定義されていないのは,単純化器実行のフェーズ数を増やしたときに「最後のフェーズまで延期されるべきインライン化や書き換え規則の適用」が誤って実行されてしまうのを防ぐためだと考えられます。「0から2まで」のような昇順になっている場合,フェーズの数を1個増やして「0から3まで」にすると,[2]という指定が最後のフェーズを指すものではなくなってしまいます。この結果,意図しない問題が発生する可能性があります。この問題を回避するにはコードを書き直す必要があり,フェーズ数を増やすことでコードの互換性が失われてしまいます。

 一方,「2から0まで」のように最後のフェーズが0に固定されていれば,[0]という指定を行うことで確実に最後のフェーズを示せます。単純化器実行のフェーズを1個増やして「3から0まで」にしても,[0]が最後のフェーズであることに変わりはありません。これにより,コードの互換性を保てます。

 なお,GHC 6.12.1のデフォルトの設定では,段階制御を使って指定するフェーズの最大値は2になっています(参考リンク)。

defaultDynFlags :: DynFlags
defaultDynFlags =
     DynFlags {
        ~ 略 ~
        verbosity               = 0,
        optLevel                = 0,
        simplPhases             = 2,
        maxSimplIterations      = 4,
        ~ 略 ~
      }

 ソースコードに3以上の値を記述することもできますが,「-fsimplifier-phases=n」というオプションを使ってフェーズの最大値を3以上に設定し直さない限り,3以上のフェーズは実際に実行されることはありません。

 なお,単純化器による「インライン化や書き換え規則の適用」の実行は,各フェーズで1回ずつしか行われないわけではありません。プログラムを効率化するために,インライン化された関数をさらにインライン化したり,書き換え規則を適用した式にさらに書き換え規則を適用したりする,といったように複数回のプログラム変換を繰り返す必要がある場合もあります。

 インライン化や書き換え規則適用の実行回数は,各フェーズを定義するのに使われている変数max_iterで与えられています。max_iterの定義は「maxSimplIterations dflags」なので,defaultDynFlagsのmaxSimplIterationsフィールドを見ればデフォルトの実行回数がわかります。GHC 6.12.1では,各フェーズで4回ずつ実行するよう設定されています。

 4回というデフォルトの実行回数は,多くのプログラムでは適切なものでしょう。しかし,プログラムによっては,4回では十分にプログラムを効率化できないこともあります。こうした場合は,「-fmax-simplifier-iterations=n」というオプションを使用することで,各フェーズでの実行回数を変更できます(参考リンク参考リンク)。

 さて,これらのオプションで本当にフェーズ数や各フェーズでの単純化器による「インライン化や書き換え規則の適用」の実行回数を変更できるかどうか,GHCのソースコードで確かめましょう。

dynamic_flags :: [Flag DynP]
dynamic_flags = [
  ~ 略 ~
  , Flag "fsimplifier-phases"
         (IntSuffix (\n -> upd (\dfs -> dfs{ simplPhases = n })))
         Supported
  , Flag "fmax-simplifier-iterations"
         (IntSuffix (\n -> upd (\dfs -> dfs{ maxSimplIterations = n })))
  ~ 略 ~
 ]
 ~ 略 ~

 dynamic_flagsの定義を見ると,simpl_phasesに使われている値のうち,phasesの設定に使われるsimplPhasesは,fsimplifier-phasesのnの値に設定されていることがわかります。同様に,max_iterの設定に使われるmaxSimplIterationsは,fmax-simplifier-iterationsのnの値に設定されています。

 では,foldrとbuildの定義に使われている段階制御に話を戻しましょう。

{-# INLINE [0] foldr #-}
~ 略 ~
foldr k z = go
          where
            go []     = z
            go (y:ys) = y `k` go ys

build   :: forall a. (forall b. (a -> b -> b) -> b -> b) -> [a]
{-# INLINE [1] build #-}
~ 略 ~
build g = g (:) []

 foldrではINLINE指示文に[0]が指定されているので,foldrのインライン化は最後のフェーズ0に到達してはじめて有効になります。buildではINLINE指示文に[1]が指定されているので,フェーズ1に到達してはじめて有効になります。

 これにより,foldrやbuildに対するインライン化が行われる前に,"fold/build"規則を使った融合変換が行われます。また,フェーズ0で有効になるfoldrのインライン化よりも,フェーズ1で有効になるbuildのインライン化のほうが先に行われます。例えば,「build (\c n -> foldr (mapFB c f) n xs)」という式は,最初に以下の形にインライン化されます。

    build (\c n -> foldr (mapFB c f) n xs)
==> { build関数のインライン化 }
    foldr (mapFB (:) f) [] xs

 段階制御はNOINLINE指示文に対しても使用できます。この場合は「指定したフェーズまではインライン化されない」という意味になります。例えば「{-# NOINLINE [1] f #-}」のように指定した場合,フェース1に到達するまでは関数fはインライン化されません。フェーズ1に到達した後のフェーズ1とフェーズ0では,関数fにはNOINLINE指示文が付いていないものとして扱われます。つまり,フェーズ1とフェーズ0では,他の関数と同様に,インライン化されるかどうかは処理系に委ねられます。

 GHCのマニュアルでは,INLINE指示文とNOINLINE指示文で段階制御を行う場合のインライン化の方針を,以下のような表にまとめています(参考リンク)。

指示文フェーズ2より前フェーズ2以降
{-# INLINE [2] f #-}INLINE化しないINLINE化を指示
{-# INLINE [~2] f #-}INLINE化を指示INLINE化しない
{-# NOINLINE [2] f #-}INLINE化しない指示なし
{-# NOINLINE [~2] f #-}指示なしINLINE化しない