web-dev-qa-db-ja.com

GHCはどのような最適化を確実に実行できると期待できますか?

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

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

私は論文を読んでいました Stream Fusion:From Lists to Streams to Nothing at Alling 、そして彼らがリスト処理を別の形式に書き換えて、GHCの通常の最適化が単純なループに確実に最適化するテクニックを使用していました私にとっては小説。自分のプログラムがそのような最適化の対象となるのはいつですか?

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

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

177
glaebhoerl

この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が再帰関数をインライン化することを拒否し、融合が機能するためには物事を正しく命名する必要があるということです。

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

  • 専門化

    特殊化の基本的な考え方は、関数が呼び出される場所を特定し、多態性ではない関数のバージョンを作成することで、ポリモーフィズムとオーバーロードを削除することです。 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で動作するものになります。これは元のものよりはるかに高速です。特殊化を見る別の方法は、型クラスの辞書と型変数への部分的な適用です。

    source には、大量のメモが含まれています。

  • フロートアウト

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

    これの基本的な考え方は、関数から繰り返してはならない計算を移動することです。たとえば、これがあったとします:

    \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
    

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

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

    現時点では、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
    

    vFalseに評価される場合、おそらく大きなサンクであるxを割り当てることにより、時間とスペースを無駄にしています。内側にフローティングするとこれが修正され、これが生成されます:

    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はcaseステートメントを許可し、float outは関数を処理します。
    2. パスの順序は固定されているため、パスが無限に変わることはありません。
  • 需要分析

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

  • Worker Wrapperバインド

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

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

    GHCのIntの定義を使用すると、

    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によっても実行できますが、ワーカー/ラッパー変換は、できることにおいて非常に一般的です。

  • 共通の部分式

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

    fib x + fib x
    

    let fib_x = fib x in fib_x + fib_x
    

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

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

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

  • 解放事件

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

    このモジュールはCoreを調べ、自由変数でcaseを探します。基準は次のとおりです。再帰呼び出しへのルートの自由変数に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はarguments既知の形式のもの。

    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番目のヒューリスティックに言及しています。

    xonlyxによって精査され、に渡されない場合にのみ、引数caseに特化する通常の関数、または結果の一部として返されます。

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

105
gereeter

怠azine

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

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

私の限られた経験では、コードを遅延させたり厳密にしたりすると、vastlyパフォーマンスのペナルティが大きくなります(時間内にandスペース)私が話そうとしている他のものより...

厳密性分析

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

落とし穴は、明らかに、コンパイラーはalwaysが厳密なものにできることを検出できないことです。時々、コンパイラに少しのヒントを与える必要があります。 (私は、Coreの出力を歩き回る以外に、厳密性分析があなたが考えていることをしたかどうかを判断する簡単な方法を知りません。)

インライン化

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

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

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

  • 呼び出している関数がクラスメソッドandである場合、コンパイラはどの型が関係しているかを知りません。たとえば、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)

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

ケース式

以下を考慮してください。

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

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

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

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

融合

リスト処理の標準的なHaskellイディオムは、1つのリストを取得して新しいリストを作成する関数を連結することです。正規の例は

map g . map f

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

問題は、これらの関数のほとんどが再帰的だということです。再帰がなければ、すべての関数を1つの大きなコードブロックに詰め込み、その上でシンプリファイアを実行し、中間リストのない本当に最適なコードを生成することが、インライン化の基本的な課題になります。しかし、再帰のために、それは機能しません。

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

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

これで、GHCはmapmapに適用されるのを見るたびに、それをリスト上の単一パスに押しつぶし、中間リストを削除します。

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

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

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


メタ:

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

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

すべてのバランスを取り、すべてを...

64

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に由来し、インライン化に関するより多くの情報が含まれています。

8
Daniel