我很好奇如何优化此代码:
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
编译器将sum
,f0
和g0
融合到构造和使用{l
的单个循环中。 1}}逐项。我可以通过手动内联来明确这一点,但那很糟糕。有没有办法明确阻止列表l
被创建和/或强制内联?在编译过程中如果内联或融合失败会产生警告或错误的编剧可能吗?
顺便说一句,我很好奇为什么seq
,inline
,lazy
等都是由前奏中的let x = x in x
定义的。这只是为了给他们一个编译器的定义来覆盖吗?
答案 0 :(得分:3)
如果你想确定,唯一的办法就是亲自去做。对于任何给定的编译器版本,您可以尝试几个源代码并检查生成的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
)很容易实现存储where l = map h [1..n]
,但如果编译器执行一些常见的子表达式消除,则重新创建它可能很难实现(不幸的是,GHC确实倾向于共享该列表形式,即使它确实很少CSE)。对于GHC,标记fno-cse
和-fno-full-laziness
可以帮助避免不必要的共享。