Pointfree版本使性能恶化

Dan*_*íaz 7 performance haskell pointfree

好吧,事实证明我在程序代码中定义了这个函数:

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
Run Code Online (Sandbox Code Playgroud)

它做了它似乎做的事情.它Stream a使用类型的内部运算符拉链(应用运算符几次,是)两个类型的元素,这是类似列表的类型a.定义非常简单.

一旦我以这种方式定义了函数,我就尝试了另一个版本:

st_zipOp :: (a -> a -> a) -> Stream a -> Stream a -> Stream a
st_zipOp = St.foldr . (st_map .)
Run Code Online (Sandbox Code Playgroud)

据我所知,这与上面的定义完全相同.它只是前一个定义的无点版本.

但是,我想检查是否有任何性能变化,我发现,实际上,无点版本使程序运行稍差(内存和时间).

为什么会这样?如果有必要,我可以编写一个再现此行为的测试程序.

我正在编译,-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
Run Code Online (Sandbox Code Playgroud)

使用opEvery(显式参数版本)的分析结果:

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

使用opEvery'(点免费版)的分析结果:

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

但是,我希望两个版本都是等价的(在所有意义上).

Dan*_*her 12

对于简单的测试用例,两个版本在使用优化进行编译时会生成相同的核心,但不进行分析.

当使用profiling enable(-prof -fprof-auto)进行编译时,pointfull版本会被内联,从而导致主要部分

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 }
Run Code Online (Sandbox Code Playgroud)

(如果没有分析,你会得到更好的东西).

在编译启用了性能分析的pointfree版本时,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
Run Code Online (Sandbox Code Playgroud)

添加{-# INLINABLE opEvery' #-}pragma时,即使在编译分析时也可以内联它

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 }
Run Code Online (Sandbox Code Playgroud)

这甚至比pragma-less pointfull版本快一点,因为它不需要勾选计数器.

这种情况可能会产生类似的效果Stream.

外卖:

  • 分析会抑制优化.没有分析的等效代码可能不具有分析支持.
  • 永远不要使用为分析或没有优化而编译的代码来衡量性能.
  • 分析可以帮助您找出在代码中花费的时间[但有时,启用分析可以完全改变代码的行为; 任何依赖于重写规则优化和/或内联的东西都是这种情况的候选者,但它无法告诉你代码的速度有多快.