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可能会引入这个问题,而且可能更难解决。