【问题标题】:Pointfree version worsens the performancePointfree 版本使性能恶化
【发布时间】:2013-01-30 17:30:06
【问题描述】:

好吧,原来我在我的程序代码中定义了这个函数:

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

它做它看起来做的事情。它使用a 类型的内部运算符压缩(多次应用运算符,是的)Stream 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)

但是,我希望这两个版本是等效的(在所有意义上)。

【问题讨论】:

  • 是的,如果您发布完整的可运行代码来演示该问题,它可能会有所帮助。编译器版本、代码使用模式等之间存在如此多的差异,仅通过模糊的描述很难提供帮助。
  • 想到的一件事是,由于应用程序已经饱和,GHC 可能更容易内联第一个版本的代码。
  • 并使用ghc -O2 -prof -fprof-auto hi.hs 编译? (这些细节很重要!)我在以这种方式编译和运行时看到了不同,所以这可能是一些分析怪癖。分析可能会干扰内联和重写规则等,所以我不会太在意这一点(除非有充分的理由关心分析运行时?)。
  • @DanielDíaz:不幸的是,为分析编译的 Haskell 代码并不能完全说明未分析代码的性能。分歧可能比这大得多。至少对于列表版本,差异完全是由于启用分析所施加的限制。
  • @shachaf 当然,这些标志的代码会有所不同(我确实检查过)。我的观点是,差异只是通过构建用于分析而施加的,而非分析的构建使用完全相同函数。

标签: performance haskell pointfree


【解决方案1】:

对于简单的测试用例,两个版本在使用优化编译时产生相同的内核,但没有进行分析。

在启用分析 (-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 }

(如果不进行分析,您会得到更好的结果)。

在启用分析的情况下编译无点版本时,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' #-} 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 }

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

Stream 案例很可能发生了类似的效果。

要点:

  • 分析会抑制优化。没有分析的等效代码可能不支持分析。
  • 切勿使用为分析或未优化而编译的代码来衡量性能。
  • 分析可以帮助您找出代码中的时间花在哪里[但有时,启用分析可以完全改变代码的行为;任何严重依赖重写规则优化和/或内联的东西都可能发生这种情况],但它不能告诉你你的代码有多快。

【讨论】:

  • 这是一个完整的答案。不过,我花了一些时间来阅读核心代码。那么结论就是 pointfree 和 pointful 代码都具有相同的性能,但是 profiling 编译会有所不同。所以信息是:请负责任地描述。
【解决方案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

【讨论】:

  • Eq 实例在这里必须做什么?为什么会有所作为?
  • 正如我所说,这只是基于假设;但是对a (无论是任何类型)给出一个约束,编译器就会知道a 不是IO () 类型的例子,它可以在某些保证的情况下转换操作的执行(顺序并不重要) .
猜你喜欢
  • 2016-07-28
  • 2012-02-01
  • 1970-01-01
  • 2017-03-18
  • 2017-12-04
  • 2021-06-09
  • 1970-01-01
  • 2018-03-30
  • 2014-12-04
相关资源
最近更新 更多