Optimization Haskell中部分计算的优化
我很好奇如何优化这段代码:Optimization Haskell中部分计算的优化,optimization,haskell,ghc,Optimization,Haskell,Ghc,我很好奇如何优化这段代码: fun n = (sum l, f $ f0 l, g $ g0 l) where l = map h [1..n] 假设f、f0、g、g0和h都很昂贵,但是l的创建和存储非常昂贵 如前所述,l被存储,直到返回的元组被完全计算或垃圾回收。相反,length l、f0 l和g0 l都应该在执行其中任何一个时执行,但是f和g应该延迟 似乎可以通过以下方式修复此行为: fun n = a `seq` b `seq` c `seq` (a, f b, g c) wh
fun n = (sum l, f $ f0 l, g $ g0 l)
where l = map h [1..n]
假设f
、f0
、g
、g0
和h
都很昂贵,但是l
的创建和存储非常昂贵
如前所述,l
被存储,直到返回的元组被完全计算或垃圾回收。相反,length l
、f0 l
和g0 l
都应该在执行其中任何一个时执行,但是f
和g
应该延迟
似乎可以通过以下方式修复此行为:
fun n = a `seq` b `seq` c `seq` (a, f b, g c)
where
l = map h [1..n]
a = sum l
b = inline f0 $ l
c = inline g0 $ l
或者非常相似的:
fun n = (a,b,c) `deepSeq` (a, f b, g c)
where ...
我们也许可以指定一组内部类型来实现同样的效果,这看起来很痛苦。还有其他选择吗
另外,我显然希望通过我的inline
s编译器将sum
、f0
和g0
融合到一个单独的循环中,该循环逐项构造和使用l
。我可以通过手动内联来明确这一点,但那太糟糕了。有没有办法明确阻止创建列表l
和/或强制内联?如果内联或融合在编译过程中失败,可能会产生警告或错误的杂注
顺便说一句,我很好奇为什么《序曲》中的
seq
、inline
、lazy
等都是由定义的。这仅仅是为了给他们一个编译器重写的定义吗?如果你想确定,唯一的方法就是自己去做。对于任何给定的编译器版本,您都可以尝试几种源代码公式,并检查生成的core/assembly/llvm字节码/无论它是否符合您的要求。但这可能会随着每个新的编译器版本而中断
如果你写信
fun n = a `seq` b `seq` c `seq` (a, f b, g c)
where
l = map h [1..n]
a = sum l
b = inline f0 $ l
c = inline g0 $ l
或者是它的deepseq
版本,编译器可能能够合并a
、b
和c
的计算,在l
的单个遍历过程中并行执行(不是并发意义上的),但目前我相当确信GHC没有,如果JHC或UHC这样做了,我会感到惊讶。为此,计算b
和c
的结构需要足够简单
要在编译器和编译器版本之间可移植地获得所需结果,唯一的方法是自己动手。至少在接下来的几年里
根据f0
和g0
的不同,它可能很简单,只需使用适当的累加器类型和组合函数进行严格的左折叠,就像著名的平均值一样
data P = P {-# UNPACK #-} !Int {-# UNPACK #-} !Double
average :: [Double] -> Double
average = ratio . foldl' count (P 0 0)
where
ratio (P n s) = s / fromIntegral n
count (P n s) x = P (n+1) (s+x)
但是如果f0
和/或g0
的结构不合适,比如说一个是左折叠,另一个是右折叠,那么可能无法在一次遍历中进行计算。在这种情况下,可以在重新创建l
和存储l
之间进行选择。通过显式共享(其中l=map h[1..n]
)很容易实现存储l
,但如果编译器消除一些常见的子表达式,则可能很难实现重新创建(不幸的是,GHC确实倾向于共享该形式的列表,即使它很少进行CSE)。对于GHC,标志fno-cse
和-fno-full laziness
可以帮助避免不必要的共享。在回答最后一个问题时:f0
和g0
完全是任意的,或者它们可以用foldr
来编写吗?简单地用(a,b,c)折叠不是很好吗-累加器在这里就足够了?我的答案正好是这样。啊,是的,如果没有正确的形式,你就不能做很多事情。我模模糊糊地想象他们两个都在l
上迭代,同时编辑一个向量,该向量总结了l
中的一些信息,可能都是根据Data.vector.acum
定义的,不确定数据。vector
可以融合两个acum
调用。啊,左右折叠的有趣之处!不过我对你的CSE观点有点困惑。您是否只是观察到,当您尝试天真地围绕列表进行编码时,CSE会造成此问题?如果重新创建列表比存储列表更便宜,您可以编写例如f0(映射h[1..n])
和g0(映射h[1..n])
。但是编译器可能会消除公共子表达式映射h[1..n]
,并在计算之间共享它。如果不希望这样做,那么防止这样做并不像相反那样简单,而是共享一个子表达式(如果您将它绑定到一个名称,,其中l=map h[1..n]
,就可以做到这一点)。基本上,是的,CSE可能会引入这个问题,而且可能更难解决。