192

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

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

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

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

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

4

3 に答える 3

116

この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)
    

    次に、以下で説明するルールが実行され、ボックス化されていない s で何かが動作することにIntなり、元のものよりもはるかに高速になります。特殊化を調べるもう 1 つの方法は、型クラス辞書と型変数への部分的な適用です。

    ここのソースには、大量のメモが含まれています。

  • 浮き出る

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

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

    \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が に評価された場合False、 を割り当てること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 floats はcaseステートメントに入れますが、float out は関数を扱います。
    2. パスの順序は固定されているため、無限に交互にする必要はありません。

  • 需要分析

    需要分析、または厳密性分析は、名前が示すように、変換ではなく、情報収集パスです。コンパイラは、引数 (または少なくともそれらの一部) を常に評価する関数を見つけ、必要に応じて呼び出すのではなく、値で呼び出すことを使用してそれらの引数を渡します。サンクのオーバーヘッドを回避できるため、多くの場合、これははるかに高速です。Haskell の多くのパフォーマンスの問題は、このパスが失敗したか、単純にコードの厳密さが不十分であることが原因で発生します。foldr簡単な例は、 、foldl、およびの使用の違いです。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
    

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

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

    ただし、llvm を介してコンパイルすると、Global Value Numbering パスにより、これらの一部が組み合わされる場合があります。

  • ケースを解放する

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

    このモジュールは を歩き回り、自由変数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、 inner 内で自由であるため、より優れたコードです。これは、既知の形式の引数を扱う SpecConstr とは異なり、自由変数を扱うことに注意してください。letrecv

    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 行) であったため、それほど多くの最適化が行われなかった可能性があります (ただし、すべての最適化が行われたと思います)。これはまた、なぜそれらのパスを選んだのか、なぜその順番に並べたのかを教えてくれません。

于 2012-09-30T01:44:27.377 に答える
66

怠惰

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

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

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

正格性解析

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

当然のことながら、問題は、コンパイラが、何かを厳密にすることができる場合を常に検出できるとは限らないということです。コンパイラにちょっとしたヒントを与える必要がある場合があります。(コア出力をくぐり抜ける以外に、厳密性分析があなたが思っていることを実行したかどうかを判断する簡単な方法を私は知りません。)

インライン化

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

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

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

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

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

ただし、新しい特殊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

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

問題は、これらの関数のほとんどが再帰的であるということです。再帰がなければ、すべての関数を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。それぞれのソリューションを手動でコーディングするのではなく、いわゆる「ストリームフュージョン」が発明されました。これはもっと複雑なトリックですが、ここでは説明しません。filterfiltermap

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

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


メタ:

  • 「最初にコードを書き、次にプロファイルを作成し、3番目に最適化する」という人々の意見に同意します。

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

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

于 2012-09-30T14:22:17.510 に答える
8

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

例外 (現在の質問のコンテキストではほとんど例外です) は、作業の重複のリスクがあるラムダです。検討:

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

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

1 回しか表示されない名前をインライン化することを検討する場合でも、コンパイラはそれを行います。

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

これは、インライン化に関するより多くの情報を含む community.haskell.org/~simonmar/papers/inline.pdf からのものです。

于 2012-11-05T14:43:01.543 に答える