好吧,事实证明我在程序代码中定义了这个函数:
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 a
的内部运算符来拉链(应用运算符几次,是)类型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)
但是,我希望两个版本都是等价的(在所有意义上)。
答案 0 :(得分:12)
对于简单的测试用例,两个版本在使用optimisations进行编译时会生成相同的内核,但不进行分析。
在启用性能分析(-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' #-}
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
案件可能会产生类似的效果。
外卖:
答案 1 :(得分:2)
对于我要说的内容,这是一个很大的假设,但我认为编译器没有足够的信息来优化您的程序。虽然没有直接回答你的问题,但是将Eq a
约束添加到两个函数(作为测试),我从pointfree变体得到了改进。见附图(拆分说明)
Right -> TOP = everyOp initial, BOTTOM = everyOp' initial
Left -> TOP = everyOp with Eq a constraint, BOTTOM = everyOp' Eq a constraint
编辑:我正在使用GHC 7.4.2