7

さて、プログラム コードでこの関数を定義したことがわかりました。

st_zipOp :: (a -> a -> a) -> Stream a -> Stream a -> Stream a
st_zipOp f xs ys = St.foldr (\x r -> st_map (f x) r) xs ys

それはそれがするように見えることをします。Stream atype の内部演算子を使用して、リストのような型であるtype の 2 つの要素を圧縮します (演算子を数回適用します、はい) a。定義は非常に簡単です。

この方法で関数を定義したら、次の別のバージョンを試しました。

st_zipOp :: (a -> a -> a) -> Stream a -> Stream a -> Stream a
st_zipOp = St.foldr . (st_map .)

私の知る限り、これは上記とまったく同じ定義です。これは、以前の定義の無意味なバージョンにすぎません。

ただし、パフォーマンスの変化があるかどうかを確認したかったのですが、実際、ポイントフリー バージョンではプログラムの実行がわずかに悪化することがわかりました (メモリと時間の両方で)。

なぜこうなった?必要に応じて、この動作を再現するテスト プログラムを作成できます。

それが違いを生むかどうかをコンパイルし-O2ています。

単純なテスト ケース

上記の動作を再現しようとして、次のコードを書きました。今回はリストを使用しましたが、パフォーマンスの変化は目立たなくなりましたが、まだ存在しています。これはコードです:

opEvery :: (a -> a -> a) -> [a] -> [a] -> [a]
opEvery f xs ys = foldr (\x r -> map (f x) r) xs ys

opEvery' :: (a -> a -> a) -> [a] -> [a] -> [a]
opEvery' = foldr . (map .)

main :: IO ()
main = print $ sum $ opEvery (+) [1..n] [1..n]
 where
  n :: Integer
  n = 5000

opEvery(明示的な引数のバージョン)を使用したプロファイリング結果:

total time  =        2.91 secs   (2906 ticks @ 1000 us, 1 processor)
total alloc = 1,300,813,124 bytes  (excludes profiling overheads)

opEvery'(ポイントフリーバージョン)を使用したプロファイリング結果:

total time  =        3.24 secs   (3242 ticks @ 1000 us, 1 processor)
total alloc = 1,300,933,160 bytes  (excludes profiling overheads)

ただし、両方のバージョンが (あらゆる意味で) 同等であることを期待していました。

4

2 に答える 2

12

単純なテスト ケースの場合、最適化を使用してコンパイルした場合、両方のバージョンで同じコアが生成されますが、プロファイリングは行われません。

プロファイリングを有効にしてコンパイルすると ( -prof -fprof-auto)、ポイントフル バージョンがインライン化され、メイン部分が

Rec {
Main.main_go [Occ=LoopBreaker]
  :: [GHC.Integer.Type.Integer] -> [GHC.Integer.Type.Integer]
[GblId, Arity=1, Str=DmdType S]
Main.main_go =
  \ (ds_asR :: [GHC.Integer.Type.Integer]) ->
    case ds_asR of _ {
      [] -> xs_r1L8;
      : y_asW ys_asX ->
        let {
          r_aeN [Dmd=Just S] :: [GHC.Integer.Type.Integer]
          [LclId, Str=DmdType]
          r_aeN = Main.main_go ys_asX } in
        scctick<opEvery.\>
        GHC.Base.map
          @ GHC.Integer.Type.Integer
          @ GHC.Integer.Type.Integer
          (GHC.Integer.Type.plusInteger y_asW)
          r_aeN
    }
end Rec }

(プロファイリングを行わなくても、より良い結果が得られます)。

プロファイリングを有効にしてポイントフリー バージョンをコンパイルすると、opEvery'インライン化されず、

Main.opEvery'
  :: forall a_aeW.
     (a_aeW -> a_aeW -> a_aeW) -> [a_aeW] -> [a_aeW] -> [a_aeW]
[GblId,
 Str=DmdType,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
         ConLike=False, WorkFree=False, Expandable=False,
         Guidance=IF_ARGS [] 80 60}]
Main.opEvery' =
  \ (@ a_c) ->
    tick<opEvery'>
    \ (x_ass :: a_c -> a_c -> a_c) ->
      scc<opEvery'>
      GHC.Base.foldr
        @ a_c
        @ [a_c]
        (\ (x1_XsN :: a_c) -> GHC.Base.map @ a_c @ a_c (x_ass x1_XsN))

Main.main4 :: [GHC.Integer.Type.Integer]
[GblId,
 Str=DmdType,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
         ConLike=False, WorkFree=False, Expandable=False,
         Guidance=IF_ARGS [] 40 0}]
Main.main4 =
  scc<main>
  Main.opEvery'
    @ GHC.Integer.Type.Integer
    GHC.Integer.Type.plusInteger
    Main.main7
    Main.main5

プラグマを追加すると{-# INLINABLE opEvery' #-}、プロファイリング用にコンパイルする場合でもインライン化できます。

Rec {
Main.main_go [Occ=LoopBreaker]
  :: [GHC.Integer.Type.Integer] -> [GHC.Integer.Type.Integer]
[GblId, Arity=1, Str=DmdType S]
Main.main_go =
  \ (ds_asz :: [GHC.Integer.Type.Integer]) ->
    case ds_asz of _ {
      [] -> lvl_r1KU;
      : y_asE ys_asF ->
        GHC.Base.map
          @ GHC.Integer.Type.Integer
          @ GHC.Integer.Type.Integer
          (GHC.Integer.Type.plusInteger y_asE)
          (Main.main_go ys_asF)
    }
end Rec }

これは、カウンターをティックする必要がないため、プラグマのない pointfull バージョンよりも少し高速です。

今回の件も同様の効果があったと考えられStreamます。

要点:

  • プロファイリングは最適化を阻害します。プロファイリングなしで同等のコードは、プロファイリングをサポートしていない場合があります。
  • プロファイリング用にコンパイルされたコード、または最適化なしでコンパイルされたコードを使用してパフォーマンスを測定しないでください。
  • プロファイリングは、コードのどこで時間が費やされているかを見つけるのに役立ちます [ただし、プロファイリングを有効にすると、コードの動作が完全に変わる場合があります。書き換えルールの最適化および/またはインライン化に大きく依存しているものは、それが起こる可能性があります]、しかし、コードがどれほど速いかはわかりません。
于 2013-01-31T12:39:45.843 に答える
2

これは私が言おうとしていることの大きな前提ですが、コンパイラーはプログラムを最適化するのに十分な情報を持っていないと思います。あなたの質問に直接答えるのではなく、Eq a(テストとして)両方の関数に制約を追加している間、無意味なバリアントから改善されました。添付画像参照(分割説明)

Right -> TOP = everyOp initial, BOTTOM = everyOp' initial
Left  -> TOP = everyOp with Eq a constraint, BOTTOM = everyOp' Eq a constraint

ここに画像の説明を入力

編集:GHC 7.4.2を使用しています

于 2013-01-31T12:00:27.933 に答える