GHCが確実に実行すると期待できる最適化は何ですか?


183

GHCには実行可能な多くの最適化がありますが、それらすべてがどのようなものであるか、またそれらがどの程度の確率で実行される可能性があるのか​​、またどのような状況で実行されるのかはわかりません。

私の質問は次のとおりです。毎回、どのような変換が適用されると期待できますか、またはほぼ同じでしょうか。頻繁に実行(評価)されるコードを見て、最初の考えが「うーん、多分それを最適化する必要がある」である場合、2番目の考えは「それについてさえ考えないでください」 GHCはこれを得た」?

私は「Stream Fusion:From Lists to Streams to Nothing at All」というペーパーを読んでいて、彼らがリスト処理を別の形式に書き換えて、GHCの通常の最適化が単純なループに確実に最適化する手法は私にとっては斬新でした。自分のプログラムがそのような最適化の対象となる時期を知るにはどうすればよいですか?

GHCマニュアルにはいくつかの情報がありますが、それは質問への回答の一部に過ぎません。

編集:私は賞金を始めています。私が欲しいのは、ラムダ/レット/ケースフローティング、タイプ/コンストラクタ/関数の引数の特殊化、厳密性分析とボックス化解除、ワーカー/ラッパー、および私が省略した他の重要なGHCのすべてのような下位レベルの変換のリストです、および入力コードと出力コードの説明と例、および理想的には、全体の効果がその部分の合計よりも多い状況の図。そして、理想的には、変換が行われない場合のいくつかの言及起こります。私はすべての変換について斬新な長さの説明を期待していません。全体像がわかる限り、2、3の文とインラインの1行のコード例(または20ページの科学論文でない場合はリンク)で十分です。それの終わりまでに明らかに。コードを見て、コンパイルしてタイトなループになるか、そうでないのか、それを作成するために何を変更する必要があるのか​​を推測できるようにしたいと思います。(ここでは、ストリームフュージョンなどの大きな最適化フレームワークにはあまり興味がありません(それについての論文を読んだだけですが、これらのフレームワークを作成する人々が持っている種類の知識に関心があります)。


10
これは最も価値のある質問です。価値のある答えを書くことは...トリッキーです。
MathematicalOrchid

1
本当に良い出発点はこれです:aosabook.org/en/ghc.html
Gabriel Gonzalez

7
どの言語でも、最初の考えが「多分それを最適化する必要がある」である場合、2番目の考えは「最初にプロファイルを作成する」です。
John L

4
あなたは後にしている知識のソートは便利ですが、これはまだ良い質問ですので、私はあなたが本当に良くてやろうとしているがサービスを提供していると思います少しできるだけ最適化。意味を書いてください。そして、パフォーマンスのためにコードを単純化すること考えなくてならないことが明らかになったときだけにしてください。コードを見て「頻繁に実行されるので、おそらく最適化する必要がある」と考えるのではなく、コードの実行が遅すぎるのを観察しているときにのみ、「頻繁に実行されるものを見つけて最適化する」べきだと考えるべきです。 。
ベン

14
私はその部分が「それをプロファイリングする」ための勧めを呼び起こすことを完全に予想していました!:)。しかし、コインの裏側は、プロファイリングして遅い場合、おそらく自分自身で手動で最適化するのではなく、まだ高レベルであるがGHCがより適切に最適化できる形式に書き換えたり、微調整したりできると思いますか?同じ種類の知識が必要です。そして、もし私がそもそもその知識を持っていれば、編集プロファイルのサイクルを省くことができたでしょう。
glaebhoerl 2012

回答:


110

このGHC Tracページもパスについてかなりよく説明しています。このページでは、最適化の順序について説明しますが、Trac Wikiの大部分と同様に、古くなっています。

詳細については、おそらく、特定のプログラムがどのようにコンパイルされているかを確認することをお勧めします。実行されている最適化を確認する最良の方法は、-vフラグを使用してプログラムを詳細にコンパイルすることです。例として、コンピュータ上で見つけたHaskellの最初の部分を見てみましょう。

Glasgow Haskell Compiler, Version 7.4.2, stage 2 booted by GHC version 7.4.1
Using binary package database: /usr/lib/ghc-7.4.2/package.conf.d/package.cache
wired-in package ghc-prim mapped to ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7
wired-in package integer-gmp mapped to integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43
wired-in package base mapped to base-4.5.1.0-6e4c9bdc36eeb9121f27ccbbcb62e3f3
wired-in package rts mapped to builtin_rts
wired-in package template-haskell mapped to template-haskell-2.7.0.0-2bd128e15c2d50997ec26a1eaf8b23bf
wired-in package dph-seq not found.
wired-in package dph-par not found.
Hsc static flags: -static
*** Chasing dependencies:
Chasing modules from: *SleepSort.hs
Stable obj: [Main]
Stable BCO: []
Ready for upsweep
  [NONREC
      ModSummary {
         ms_hs_date = Tue Oct 18 22:22:11 CDT 2011
         ms_mod = main:Main,
         ms_textual_imps = [import (implicit) Prelude, import Control.Monad,
                            import Control.Concurrent, import System.Environment]
         ms_srcimps = []
      }]
*** Deleting temp files:
Deleting: 
compile: input file SleepSort.hs
Created temporary directory: /tmp/ghc4784_0
*** Checking old interface for main:Main:
[1 of 1] Compiling Main             ( SleepSort.hs, SleepSort.o )
*** Parser:
*** Renamer/typechecker:
*** Desugar:
Result size of Desugar (after optimization) = 79
*** Simplifier:
Result size of Simplifier iteration=1 = 87
Result size of Simplifier iteration=2 = 93
Result size of Simplifier iteration=3 = 83
Result size of Simplifier = 83
*** Specialise:
Result size of Specialise = 83
*** Float out(FOS {Lam = Just 0, Consts = True, PAPs = False}):
Result size of Float out(FOS {Lam = Just 0,
                              Consts = True,
                              PAPs = False}) = 95
*** Float inwards:
Result size of Float inwards = 95
*** Simplifier:
Result size of Simplifier iteration=1 = 253
Result size of Simplifier iteration=2 = 229
Result size of Simplifier = 229
*** Simplifier:
Result size of Simplifier iteration=1 = 218
Result size of Simplifier = 218
*** Simplifier:
Result size of Simplifier iteration=1 = 283
Result size of Simplifier iteration=2 = 226
Result size of Simplifier iteration=3 = 202
Result size of Simplifier = 202
*** Demand analysis:
Result size of Demand analysis = 202
*** Worker Wrapper binds:
Result size of Worker Wrapper binds = 202
*** Simplifier:
Result size of Simplifier = 202
*** Float out(FOS {Lam = Just 0, Consts = True, PAPs = True}):
Result size of Float out(FOS {Lam = Just 0,
                              Consts = True,
                              PAPs = True}) = 210
*** Common sub-expression:
Result size of Common sub-expression = 210
*** Float inwards:
Result size of Float inwards = 210
*** Liberate case:
Result size of Liberate case = 210
*** Simplifier:
Result size of Simplifier iteration=1 = 206
Result size of Simplifier = 206
*** SpecConstr:
Result size of SpecConstr = 206
*** Simplifier:
Result size of Simplifier = 206
*** Tidy Core:
Result size of Tidy Core = 206
writeBinIface: 4 Names
writeBinIface: 28 dict entries
*** CorePrep:
Result size of CorePrep = 224
*** Stg2Stg:
*** CodeGen:
*** CodeOutput:
*** Assembler:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-I.' '-c' '/tmp/ghc4784_0/ghc4784_0.s' '-o' 'SleepSort.o'
Upsweep completely successful.
*** Deleting temp files:
Deleting: /tmp/ghc4784_0/ghc4784_0.c /tmp/ghc4784_0/ghc4784_0.s
Warning: deleting non-existent /tmp/ghc4784_0/ghc4784_0.c
link: linkables are ...
LinkableM (Sat Sep 29 20:21:02 CDT 2012) main:Main
   [DotO SleepSort.o]
Linking SleepSort ...
*** C Compiler:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-c' '/tmp/ghc4784_0/ghc4784_0.c' '-o' '/tmp/ghc4784_0/ghc4784_0.o' '-DTABLES_NEXT_TO_CODE' '-I/usr/lib/ghc-7.4.2/include'
*** C Compiler:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-c' '/tmp/ghc4784_0/ghc4784_0.s' '-o' '/tmp/ghc4784_0/ghc4784_1.o' '-DTABLES_NEXT_TO_CODE' '-I/usr/lib/ghc-7.4.2/include'
*** Linker:
'/usr/bin/gcc' '-fno-stack-protector' '-Wl,--hash-size=31' '-Wl,--reduce-memory-overheads' '-o' 'SleepSort' 'SleepSort.o' '-L/usr/lib/ghc-7.4.2/base-4.5.1.0' '-L/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0' '-L/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0' '-L/usr/lib/ghc-7.4.2' '/tmp/ghc4784_0/ghc4784_0.o' '/tmp/ghc4784_0/ghc4784_1.o' '-lHSbase-4.5.1.0' '-lHSinteger-gmp-0.4.0.0' '-lgmp' '-lHSghc-prim-0.2.0.0' '-lHSrts' '-lm' '-lrt' '-ldl' '-u' 'ghczmprim_GHCziTypes_Izh_static_info' '-u' 'ghczmprim_GHCziTypes_Czh_static_info' '-u' 'ghczmprim_GHCziTypes_Fzh_static_info' '-u' 'ghczmprim_GHCziTypes_Dzh_static_info' '-u' 'base_GHCziPtr_Ptr_static_info' '-u' 'base_GHCziWord_Wzh_static_info' '-u' 'base_GHCziInt_I8zh_static_info' '-u' 'base_GHCziInt_I16zh_static_info' '-u' 'base_GHCziInt_I32zh_static_info' '-u' 'base_GHCziInt_I64zh_static_info' '-u' 'base_GHCziWord_W8zh_static_info' '-u' 'base_GHCziWord_W16zh_static_info' '-u' 'base_GHCziWord_W32zh_static_info' '-u' 'base_GHCziWord_W64zh_static_info' '-u' 'base_GHCziStable_StablePtr_static_info' '-u' 'ghczmprim_GHCziTypes_Izh_con_info' '-u' 'ghczmprim_GHCziTypes_Czh_con_info' '-u' 'ghczmprim_GHCziTypes_Fzh_con_info' '-u' 'ghczmprim_GHCziTypes_Dzh_con_info' '-u' 'base_GHCziPtr_Ptr_con_info' '-u' 'base_GHCziPtr_FunPtr_con_info' '-u' 'base_GHCziStable_StablePtr_con_info' '-u' 'ghczmprim_GHCziTypes_False_closure' '-u' 'ghczmprim_GHCziTypes_True_closure' '-u' 'base_GHCziPack_unpackCString_closure' '-u' 'base_GHCziIOziException_stackOverflow_closure' '-u' 'base_GHCziIOziException_heapOverflow_closure' '-u' 'base_ControlziExceptionziBase_nonTermination_closure' '-u' 'base_GHCziIOziException_blockedIndefinitelyOnMVar_closure' '-u' 'base_GHCziIOziException_blockedIndefinitelyOnSTM_closure' '-u' 'base_ControlziExceptionziBase_nestedAtomically_closure' '-u' 'base_GHCziWeak_runFinalizzerBatch_closure' '-u' 'base_GHCziTopHandler_flushStdHandles_closure' '-u' 'base_GHCziTopHandler_runIO_closure' '-u' 'base_GHCziTopHandler_runNonIO_closure' '-u' 'base_GHCziConcziIO_ensureIOManagerIsRunning_closure' '-u' 'base_GHCziConcziSync_runSparks_closure' '-u' 'base_GHCziConcziSignal_runHandlers_closure'
link: done
*** Deleting temp files:
Deleting: /tmp/ghc4784_0/ghc4784_1.o /tmp/ghc4784_0/ghc4784_0.s /tmp/ghc4784_0/ghc4784_0.o /tmp/ghc4784_0/ghc4784_0.c
*** Deleting temp dirs:
Deleting: /tmp/ghc4784_0

最初*** Simplifier:から最後まで、すべての最適化フェーズが発生するところを見ると、かなり多くのことがわかります。

まず、Simplifierはほとんどすべてのフェーズ間で実行されます。これにより、多くのパスを簡単に記述できます。たとえば、多くの最適化を実装する場合、手動で行う代わりに、変更を伝達するための書き換えルールを作成するだけです。簡略化機能は、インライン化と融合を含む、いくつかの単純な最適化を含みます。私が知っているこの主な制限は、GHCが再帰関数のインライン化を拒否していること、およびFusionが機能するためには、物事を正しく命名する必要があることです。

次に、実行されたすべての最適化の完全なリストが表示されます。

  • 特化

    特殊化の基本的な考え方は、関数が呼び出される場所を特定し、多態性ではないバージョンの関数を作成することで、多態性とオーバーロードを削除することです。これらは、呼び出される型に固有です。SPECIALISEプラグマを使用してこれを行うようコンパイラーに指示することもできます。例として、階乗関数を考えます。

    fac :: (Num a, Eq a) => a -> a
    fac 0 = 1
    fac n = n * fac (n - 1)

    コンパイラは、使用される乗算のプロパティを認識していないため、これを最適化することはできません。ただし、それがで使用されている場合はInt、タイプのみが異なる新しいバージョンを作成できます。

    fac_Int :: Int -> Int
    fac_Int 0 = 1
    fac_Int n = n * fac_Int (n - 1)

    次に、以下で説明するルールが実行され、ボックス化されていないIntsで機能するものになり、元のルールよりもはるかに高速になります。特殊化の別の見方は、型クラス辞書と型変数の部分的な適用です。

    ここのソースには、たくさんのメモが含まれています。

  • フロートアウト

    編集:私は明らかにこれを以前に誤解していました。私の説明は完全に変わりました。

    これの基本的な考え方は、繰り返されるべきではない計算を関数の外に移動することです。たとえば、次のようにしたとします。

    \x -> let y = expensive in x+y

    上記のラムダでは、関数が呼び出されるたびに、y再計算されます。浮動小数点が生成するより良い関数は、

    let y = expensive in \x -> x+y

    プロセスを容易にするために、他の変換を適用することができます。たとえば、これは起こります:

     \x -> x + f 2
     \x -> x + let f_2 = f 2 in f_2
     \x -> let f_2 = f 2 in x + f_2
     let f_2 = f 2 in \x -> x + f_2

    繰り返しになりますが、繰り返し計算が保存されます。

    ソースは、この場合には非常に読みやすいです。

    現時点では、2つの隣接するラムダ間のバインディングはフローティングされていません。たとえば、これは起こりません:

    \x y -> let t = x+x in ...

    に行く

     \x -> let t = x+x in \y -> ...
  • 内側にフロート

    ソースコードを引用すると、

    の主な目的はfloatInwards、ケースのブランチにフロートすることです。これにより、物事を割り当てたり、スタックに保存したりせず、選択されたブランチでそれらが必要ないことがわかります。

    例として、次の式があるとします。

    let x = big in
        case v of
            True -> x + 1
            False -> 0

    がとv評価された場合Falsexおそらく何らかの大きなサンクであるを割り当てることにより、時間とスペースを無駄にしています。内側にフローティングするとこれが修正され、次のようになります。

    case v of
        True -> let x = big in x + 1
        False -> let x = big in 0

    、これはその後、

    case v of
        True -> big + 1
        False -> 0

    このペーパーでは、他のトピックもカバーしていますが、かなり明確な紹介を提供しています。それらの名前にもかかわらず、2つの理由により、フローティングインとフローティングアウトは無限ループに入りません。

    1. Float in floatはlet into caseステートメントになり、float outは関数を扱います。
    2. パスの順序は決まっているので、パスが無限に変化することはありません。

  • 需要分析

    需要分析、つまり厳密性分析は、名前が示すように、情報収集パスの変形ではなく、多くの変形です。コンパイラーは常に引数(または少なくとも一部)を評価する関数を見つけ、call-by-needではなく、call-by-valueを使用してそれらの引数を渡します。サンクのオーバーヘッドを回避できるため、多くの場合、これははるかに高速です。Haskellの多くのパフォーマンスの問題は、このパスが失敗したか、コードが厳密でないために発生します。簡単な例では、使用の間の差であるfoldrfoldlおよびfoldl'整数のリストを合計します-最初はスタックオーバーフローを引き起こし、2番目はヒープオーバーフローを引き起こし、最後は厳密さのために正常に実行されます。これはおそらく、これらすべての中で最も理解しやすく、最もよく文書化されています。私は、ポリモーフィズムとCPSコードがこれを打ち負かすことが多いと思います。

  • ワーカーラッパーバインド

    ワーカー/ラッパー変換の基本的な考え方は、単純な構造でタイトループを実行し、最後にその構造との間で変換を行うことです。たとえば、数値の階乗を計算するこの関数を見てみましょう。

    factorial :: Int -> Int
    factorial 0 = 1
    factorial n = n * factorial (n - 1)

    IntGHC の定義を使用して、

    factorial :: Int -> Int
    factorial (I# 0#) = I# 1#
    factorial (I# n#) = I# (n# *# case factorial (I# (n# -# 1#)) of
        I# down# -> down#)

    コードがI#sでどのようにカバーされているかに注意してください。これを行うことでそれらを削除できます:

    factorial :: Int -> Int
    factorial (I# n#) = I# (factorial# n#)
    
    factorial# :: Int# -> Int#
    factorial# 0# = 1#
    factorial# n# = n# *# factorial# (n# -# 1#)

    この特定の例はSpecConstrでも実行できた可能性がありますが、ワーカー/ラッパー変換は、実行できることにおいて非常に一般的です。

  • 一般的な部分式

    これは、厳密性分析のように、非常に効果的なもう1つの本当に単純な最適化です。基本的な考え方は、同じ2つの式がある場合、それらは同じ値を持つということです。たとえばfib、フィボナッチ数計算機の場合、CSEは変換します

    fib x + fib x

    let fib_x = fib x in fib_x + fib_x

    これにより、計算が半分になります。残念ながら、これは他の最適化の邪魔になることがあります。別の問題は、2つの式が同じ場所にある必要があり、値によって同じではなく構文的に同じでなければならないことです。たとえば、CSEは次のコードでは大量のインライン化なしでは起動しません。

    x = (1 + (2 + 3)) + ((1 + 2) + 3)
    y = f x
    z = g (f x) y

    ただし、llvmを介してコンパイルする場合、グローバル値番号付けパスのために、これらの一部が結合される可能性があります。

  • 解放事件

    これは、コードの爆発を引き起こす可能性があるという事実を除けば、ひどく文書化された変換のようです。ここに私が見つけた小さなドキュメントの再フォーマットされた(そして少し書き直された)バージョンがあります:

    このモジュールはをウォークスルーしCorecase自由変数を探します。基準は次のとおりです。case再帰呼び出しへのルートにフリー変数が存在する場合、再帰呼び出しは展開で置き換えられます。たとえば、

    f = \ t -> case v of V a b -> a : f t

    インナーfが交換されます。作る

    f = \ t -> case v of V a b -> a : (letrec f = \ t -> case v of V a b -> a : f t in f) t

    シャドウイングの必要性に注意してください。単純化して、

    f = \ t -> case v of V a b -> a : (letrec f = \ t -> a : f t in f t)

    これは、からの投影を必要とするのではなくa、内部letrecで自由であるため、より優れたコードvです。これは、既知の形式の引数を処理するSpecConstrとは異なり、自由変数を処理することに注意してください。

    SpecConstrの詳細については、以下を参照してください。

  • SpecConstr-これはプログラムを次のように変換します

    f (Left x) y = somthingComplicated1
    f (Right x) y = somethingComplicated2

    f_Left x y = somethingComplicated1
    f_Right x y = somethingComplicated2
    
    {-# INLINE f #-}
    f (Left x) = f_Left x
    f (Right x) = f_Right x

    拡張された例として、次の定義を取り上げますlast

    last [] = error "last: empty list"
    last (x:[]) = x
    last (x:x2:xs) = last (x2:xs)

    最初にそれを

    last_nil = error "last: empty list"
    last_cons x [] = x
    last_cons x (x2:xs) = last (x2:xs)
    
    {-# INLINE last #-}
    last [] = last_nil
    last (x : xs) = last_cons x xs

    次に、単純化器が実行され、

    last_nil = error "last: empty list"
    last_cons x [] = x
    last_cons x (x2:xs) = last_cons x2 xs
    
    {-# INLINE last #-}
    last [] = last_nil
    last (x : xs) = last_cons x xs

    リストの前面のボックス化とボックス化解除を繰り返し行わないため、プログラムがより高速になることに注意してください。また、インライン化は、再帰的な定義を改善するだけでなく、新しくより効率的な定義を実際に使用できるため、非常に重要です。

    SpecConstrは、いくつかのヒューリスティックによって制御されます。論文で言及されているものは次のとおりです。

    1. ラムダは明示的であり、アリティはaです。
    2. 右側は「十分に小さい」もので、旗によって制御されています。
    3. この関数は再帰的で、特殊化可能な呼び出しが右側で使用されています。
    4. 関数へのすべての引数が存在します。
    5. 少なくとも1つの引数がコンストラクターアプリケーションです。
    6. その引数は、関数のどこかでケース分析されます。

    ただし、ヒューリスティックはほぼ確実に変更されています。実際、このペーパーでは、代替の6番目のヒューリスティックについて言及しています。

    引数に特化xする場合にのみxされるだけで精査caseし、通常の関数に渡される、または結果の一部として返されません。

これは非常に小さなファイル(12行)だったので、それほど多くの最適化をトリガーしなかった可能性があります(ただし、すべての最適化は行われたと思います)。これは、なぜそれらのパスを選択したのか、なぜそれらをその順序で配置したのかを教えてくれません。


今、私たちはどこかに着いています!コメント:スペシャライズに関する部分に切り捨て文があるようです。フロートアウトのポイントがわかりません。それは何のためですか?フロートするかフロートしないかはどうやって決定するのですか(なぜループに入らないのですか)?どこかでGHCがCSE をまったくしなかった印象がありましたが、どうやらそれは間違いでした。全体像を見るのではなく、詳細に迷い込んでいるような気がします...トピックは思ったよりもさらに複雑です。たぶん私の質問は不可能であり、大量の経験またはGHCで自分自身で作業することを除いて、この直感を得る方法はありませんか?
glaebhoerl

えーと、わかりませんが、GHCに取り組んだことがないので、ある程度の直感が得られるはずです。
gereeter

あなたが言及した問題を修正しました。
gereeter

1
また、全体像については、実際にはないというのが私の意見です。どの最適化が実行されるかを推測したい場合は、チェックリストを調べます。次に、もう一度実行して、各パスがどのように変化するかを確認します。そしてまた。基本的に、私はコンパイラーを再生します。私が知っている唯一の最適化スキームが本当に「全体像」を持っているのは、スーパーコンパイルです。
gereeter

1
「フュージョンを機能させるには、正しく名前を付ける必要がある」とはどういう意味ですか?
Vincent Beffara、2014

65

怠惰

これは「コンパイラの最適化」ではありませんが、言語仕様によって保証されているため、いつでもそれを期待できます。基本的に、これは、結果で「何かを行う」まで作業が実行されないことを意味します。(意図的に怠惰をオフにするためにいくつかのことの1つを行わない限り)。

これは明らかに、それ自体が全体のトピックであり、SOにはすでにそれに関する多くの質問と回答があります。

私の限られた経験では、コードを怠惰にしたり厳格にしたりすると、これから説明する他のどの要素よりも(時間空間で)パフォーマンスが大幅に低下します...

厳格な分析

怠惰はそれが必要でない限り仕事を避けることです。コンパイラは、特定の結果が「常に」必要であると判断できる場合、計算を保存して後で実行する必要はありません。より効率的であるため、直接実行するだけです。これは、いわゆる「厳密性分析」です。

落とし穴は、明らかに、コンパイラが何かを厳密にすることができるときを常に検出できるわけではないということです。コンパイラに少しのヒントを与える必要がある場合があります。(厳密な分析が、コア出力をたどること以外に、あなたが思っていることを行ったかどうかを判断する簡単な方法は知りません。)

インライン化

関数を呼び出し、コンパイラが呼び出している関数を判別できる場合、コンパイラはその関数を「インライン化」しようとする可能性があります。つまり、関数呼び出しを関数自体のコピーに置き換えようとします。通常、関数呼び出しのオーバーヘッドはかなり小さいですが、インライン化によって他の方法では実現できなかった他の最適化が行われることがよくあるため、インライン化は大きなメリットになります。

関数がインライン化されるのは、「十分に小さい」場合(またはインライン化を要求するプラグマを追加した場合)のみです。また、関数がインライン化できるのは、コンパイラーが呼び出している関数を判別できる場合のみです。コンパイラが判断できない主な方法が2つあります。

  • 呼び出す関数が別の場所から渡された場合。たとえば、filter関数がコンパイルされるとき、ユーザーが指定した引数であるため、フィルター述語をインライン化することはできません。

  • 呼び出す関数がクラスメソッドであり、コンパイラーがどの型が関係しているかがわからない場合。たとえば、sum関数がコンパイルされると、コンパイラは+関数をインライン化できません。sumこれは、それぞれが異なる+関数を持ついくつかの異なる数値型で動作するためです。

後者の場合、{-# SPECIALIZE #-}プラグマを使用して、特定の型にハードコードされた関数のバージョンを生成できます。たとえば、タイプ用{-# SPECIALIZE sum :: [Int] -> Int #-}sumハードコードされたバージョンをコンパイルします。Intつまり+、このバージョンでインライン化できます。

ただし、新しい特殊sum関数は、コンパイラーがで作業していることを認識できる場合にのみ呼び出されることに注意してくださいInt。それ以外の場合は、元の多態性sumが呼び出されます。この場合も、実際の関数呼び出しのオーバーヘッドはかなり小さくなっています。インライン化によって実現できる追加の最適化は、有益です。

共通部分式の除去

コードの特定のブロックが同じ値を2回計算する場合、コンパイラはそれを同じ計算の単一のインスタンスに置き換えます。たとえば、

(sum xs + 1) / (sum xs + 2)

次に、コンパイラはこれを最適化します

let s = sum xs in (s+1)/(s+2)

コンパイラが常にこれ行うと期待するかもしれません。ただし、明らかに一部の状況では、これによりパフォーマンスが低下する可能性があり、パフォーマンスが低下する可能性があるため、GHCが常にこれを行うとは限りません。率直に言って、私はこの背後にある詳細を本当に理解していません。しかし、肝心なのは、この変換が重要である場合、手動で行うことは難しくありません。(そして、それが重要でないなら、なぜあなたはそれについて心配していますか?)

ケース式

以下を検討してください。

foo (0:_ ) = "zero"
foo (1:_ ) = "one"
foo (_:xs) = foo xs
foo (  []) = "end"

最初の3つの方程式はすべて、リストが空でないかどうかを確認します(特に)。しかし、同じことを3回チェックするのは無駄です。幸い、コンパイラーがこれをいくつかのネストされたケース式に最適化するのは非常に簡単です。この場合、次のようなもの

foo xs =
  case xs of
    y:ys ->
      case y of
        0 -> "zero"
        1 -> "one"
        _ -> foo ys
    []   -> "end"

これはかなり直感的ではありませんが、より効率的です。コンパイラーはこの変換を簡単に行うことができるため、心配する必要はありません。できるだけ直感的な方法でパターンマッチングを記述してください。コンパイラーは、これを可能な限り高速にするためにこれを再配列および再配置するのに非常に優れています。

融合

リスト処理の標準Haskellイディオムは、1つのリストを取得して新しいリストを生成する関数をチェーン化することです。正規の例は

map g . map f

残念ながら、遅延は不必要な作業のスキップを保証しますが、中間リストのすべての割り当てと割り当て解除はsapパフォーマンスです。「融合」または「森林破壊」は、コンパイラがこれらの中間ステップを排除しようとする場所です。

問題は、これらの関数のほとんどが再帰的であることです。再帰がなければ、すべての関数を1つの大きなコードブロックに圧縮し、その上で単純化子を実行し、中間リストのない本当に最適なコードを生成することは、インライン化の基本的な演習になります。しかし、再帰のため、それは機能しません。

{-# RULE #-}これを修正するためにプラグマを使用できます。例えば、

{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-}

これで、GHCがにmap適用されるのを見るたびにmap、リストを1回のパスに圧縮して中間リストを削除します。

問題は、これはのmap後にのみ機能することmapです。他にも多くの可能性があります-がmap続きfilter、がfilter続きますmap。それぞれのソリューションを手動でコーディングするのではなく、いわゆる「ストリームフュージョン」が発明されました。これはより複雑なトリックであり、ここでは説明しません。

長い点と短い点:これらはすべて、プログラマーが作成し特別な最適化トリックです。GHC自体は融合について何も知りません。リストライブラリとその他のコンテナライブラリにすべて含まれています。したがって、どの最適化が行われるかは、コンテナーライブラリの作成方法(より現実的には、使用するライブラリを選択する)によって異なります。

たとえば、Haskell '98の配列を使用する場合、いかなる種類の融合も期待しないでください。しかし、vectorライブラリには広範な融合機能があることを理解しています。それはすべてライブラリに関するものです。コンパイラはRULESプラグマを提供するだけです。(ちなみに、これは非常に強力です。ライブラリの作成者は、これを使用してクライアントコードを書き換えることができます。)


メタ:

  • 「最初にコード、2番目にプロファイル、3番目に最適化」と言っている人々に同意します。

  • 私はまた、「与えられた設計の決定がどれだけのコストを持つかについてのメンタルモデルを持つことは有用である」と言っている人々にも同意します。

すべてのもの、そしてすべてのもののバランス...


9
it's something guaranteed by the language specification ... work is not performed until you "do something" with the result.- ではない正確に。言語仕様は非厳密なセマンティクスを約束します。余分な作業が実行されるかどうかについては何も約束しません。
Dan Burton

1
@DanBurtonもちろん。しかし、これを数文で説明するのは簡単ではありません。さらに、GHCは現存するHaskell実装のほとんど唯一であるため、GHCが遅延であるという事実は、ほとんどの人にとって十分なものです。
MathematicalOrchid

@MathematicalOrchid:投機的評価は興味深い反例ですが、初心者には多すぎると思います。
ベンミルウッド、2012年

5
CSEについて:私の印象では、それは不要な共有とそれによるスペースリークを引き起こす可能性があるため、ほとんど行われないということです。
Joachim Breitner、2012年

2
(a)今までに返信しないこと、および(b)回答を受け入れないことをお詫び申し上げます。これは長くて印象的ですが、私が望んでいた地域をカバーしていません。私が欲しいのは、ラムダ/レット/ケースフローティング、タイプ/コンストラクタ/関数の引数の特殊化、厳密性分析とボックス化解除(あなたが言及している)、ワーカー/ラッパー、およびGHCが行う他のすべてのような低レベル変換のリストです入力コードと出力コードの説明と例、理想的にはそれらを組み合わせた効果の例と変換発生しない例を示します。私は賞金を稼ぐべきだと思いますか?
glaebhoerl 2012年

8

letバインディングv = rhsが1か所だけで使用されている場合、rhsが大きくても、コンパイラーをインライン化して数えることができます。

例外(現在の質問のコンテキストではほとんどありません)は、作業の重複を危険にさらすラムダです。考慮してください:

let v = rhs
    l = \x-> v + x
in map l [1..100]

そこでは、vのインライン化は危険です。なぜなら、1つ(構文)を使用すると、rhsの追加の99回の評価に変換されるためです。ただし、この場合、手動でインライン化することはほとんどありません。したがって、基本的にはルールを使用できます。

一度しか出現しない名前をインライン化することを検討する場合、コンパイラはとにかくそれを行います。

幸いな結果として、letバインディングを使用して長いステートメントを単純に分解すること(明確にすることを期待して)は基本的に無料です。

これは、インライン化に関する多くの情報を含むcommunity.haskell.org/~simonmar/papers/inline.pdfに由来しています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.