3

Disclaimer: I know little about ghc compiling pipeline, but I hope to learn some more about it with this post, for example, if comparing imperative vs functional is relevant to code compilation.

As you know, loop unrolling reduces the number of iterations over a loop by duplicating the code inside it. This improves performance since it reduces the number of jumps (and penalities associated with it) and AFAIR, creates bigger blocks of code, leaving room for better Register renaming optimization.

I was wondering, could there be an equivalent to Loop Unrolling for functional programming? Could we 'unroll' a function, open/expand it's definition, to first reduce the number of calls to said function and/or creating bigger chunks of code -- then leaving room for more code rewrite optimizations (like register renaming, or some FP equivalent)?

Something that would 'unroll' or 'expand' a function definition, using for example function evaluation (maybe mixed with some tactic) in order to have a trade-off between space vs time.

An example of what I had in mind:

 map1 _ [] = []
 map1 f (x:xs) = (f x): map f xs

Would unroll to

map2 _ [] = []
map2 f (x:x1:xs) = (f x):(f x1):map2 f xs
map2 f (x:xs) = (f x): map2 f xs

Once more:

map4 _ [] = []
map4 f (x:x1:x2:x3:xs) = (f x):(f x1):(f x2):(f x3):map4 f xs
map4 f (x:x1:x2:xs) = (f x):(f x1):(f x2):map4 f xs
map4 f (x:x1:xs) = (f x):(f x1):map4 f xs
map4 f (x:xs) = (f x): map4 f xs

Two things are at play: multiple cases of map4 (and consequent tests on list) could degrade performance, or the reduced number of calls of map4 would improve performance. Maybe this could reduce some constant overhead created by lazy evaluation?

Well that doesn't seems to hard to code a test for, so after putting criterion to roll this out, this is what I've got:

ImgUr album

Problem size 5*10^6

map  105.4 ms
map2 93.34 ms
map4 89.79 ms

Problem size 1*10^7

map  216.3 ms
map2 186.8 ms
map4 180.1 ms

Problem size 5*10^7

map  1050 ms
map2 913.7 ms
map4 899.8 ms

Well, it seems that unrolling had some effect^1! map4 appears to be 16% faster.

Time for the questions then:

  1. Have this been discussed before? Is something like that already implemented?
  2. Is it really the reduced number of evaluations of map4 that improves speed?
  3. Can this be automated?
  4. Could I evaluate by chunks? ie.: if (f x) is fully evaluated, fully evalute everything up to (f x4).
  5. Any other form this sort of unrolling come at play?
  6. How inflation on the function size could this lead to?
  7. Any short-commings on why this is not a good idea?

1: I`ve also unrolled fib, since this sort of optimization would also happen in that form, but the performance gain is cheating a (very) bad algorithm.

4

3 に答える 3

6

最適化してコンパイルしましたか?私にとって、 with -O2、これらのスニペット間に実際の違いはありません: map1map2、およびmap4は、それぞれ 279 ミリ秒、267 ミリ秒、285 ミリ秒で実行されました (比較のために、mapそれ自体は 278 ミリ秒で実行されました)。そのため、私には改善ではなく、測定ノイズのように見えます。

そうは言っても、ループ展開に関すると思われるこのGHCプラグインを見たいと思うかもしれません。

悲しいことに、純粋関数型言語と命令型言語の最適化手法が大きく異なる傾向があることは事実です。たとえば、ストリーム フュージョンと森林伐採に注目するとよいでしょう。この 2 つの手法は非常に優れていますが、命令型言語にはうまく変換できません。

そして、「なぜこれが良い考えではないのかについての短所はありますか?」については、まあ、すぐに思いつくことができます:

*Main> map succ (1:undefined)
[2*** Exception: Prelude.undefined
*Main> map4 succ (1:undefined)
*** Exception: Prelude.undefined

多くの場合、パフォーマンスを向上させるために関数をより厳密にすることは問題ありませんが、ここではパフォーマンスの向上が明確ではなくmap、怠惰に依存する方法で使用されることがよくあります。

于 2013-09-30T03:57:56.210 に答える
3

すでに述べた ghc アンローリング プラグインに加えて、ピーリング/アンローリングについて説明しているページが GHC tracにあります。「未解決の問題」と「参考文献」セクションは、特にさらなる研究資料の情報源を明らかにしています。

于 2013-09-30T05:25:10.170 に答える
1

ループ展開は非常に鈍器です。たとえば、あなたのmap例が展開されることは決してありません。返されたリストとそのセル内のサンクのメモリ割り当てによって完全に支配されます。レジスタ アロケータがもっと噛む必要があるかどうかは問題ではありません。(折り目を展開するかどうかfoldl'は、おそらく別の問題です。)

GHC は、再帰関数をインライン展開することでループ展開を実現できます。しかし、そうしないように努力しています: 実際、再帰グループの「ループブレーカー」をインライン化することはありません。それ以外の場合、インライン化が終了するという保証はありません。「GHC インライナーの秘密」のセクション 4 を参照してください。

GHCLiberateCase、そのパス ( で実行) に限定された形式のループ ピーリング (または、部分的な冗長性の除去) を適用します-O2

f :: (Int, Int) -> Int
f p = g [0..snd p]
  where
    g :: [Int] -> Int
    g [] = 0
    g (x:xs) = fst p + x + g xs

ここで、GHC はループの 1 回の繰り返しをピールして、fst p投影をループから取り出し、代わりに unboxed を参照しますInt#。芯:

Lib.$wf
  = \ (ww_sNF :: Int) (ww1_sNJ :: GHC.Prim.Int#) ->
      case GHC.Enum.eftInt 0# ww1_sNJ of {
        [] -> 0#;
        : x_atE xs_atF ->
          case ww_sNF of { GHC.Types.I# x1_aLW ->
          case x_atE of { GHC.Types.I# y_aLZ ->
          letrec {
            $wg_sNB [InlPrag=NOUSERINLINE[2], Occ=LoopBreaker]
              :: [Int] -> GHC.Prim.Int#
            [LclId, Arity=1, Str=<S,1*U>, Unf=OtherCon []]
            $wg_sNB
              = \ (w_sNx :: [Int]) ->
                  case w_sNx of {
                    [] -> 0#;
                    : x2_Xud xs1_Xuf ->
                      case x2_Xud of { GHC.Types.I# y1_XMG ->
                      case $wg_sNB xs1_Xuf of ww2_sNA { __DEFAULT ->
                      GHC.Prim.+# (GHC.Prim.+# x1_aLW y1_XMG) ww2_sNA
                      }
                      }
                  }; } in
          case $wg_sNB xs_atF of ww2_sNA { __DEFAULT ->
          GHC.Prim.+# (GHC.Prim.+# x1_aLW y_aLZ) ww2_sNA
          }
          }
          }
      }
于 2021-10-26T13:05:37.967 に答える