在 Haskell 中优化部分计算

Posted

技术标签:

【中文标题】在 Haskell 中优化部分计算【英文标题】:Optimizing partial computation in Haskell 【发布时间】:2012-04-22 09:54:46 【问题描述】:

我很好奇如何优化这段代码:

fun n = (sum l, f $ f0 l, g $ g0 l)
  where l = map h [1..n]

假设ff0gg0h都是昂贵的,但是l的创建和存储是极其昂贵的。

正如所写,l 将被存储,直到返回的元组被完全评估或垃圾收集。相反,length lf0 lg0 l 应该在它们中的任何一个被执行时都被执行,但 fg 应该被延迟。

看来这种行为可以通过写作来解决:

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 ...

我们也许可以指定一堆内部类型来实现相同的效果,这看起来很痛苦。还有其他选择吗?

另外,我显然希望我的inlines 编译器将sumf0g0 融合到一个循环中,逐个构造和使用l。我可以通过手动内联来明确这一点,但这很糟糕。有没有办法明确阻止列表l 被创建和/或强制内联?如果编译期间内联或融合失败,可能会产生警告或错误的编译指示?

顺便说一句,我很好奇为什么seqinlinelazy 等都是由前奏曲中的let x = x in x 定义的。这仅仅是为了给它们一个定义让编译器覆盖吗​​?

【问题讨论】:

回复最后一个问题:***.com/a/8654407/1011995 f0g0 完全是任意的,还是可以写成foldr 这里用 (a,b,c) 累加器的简单折叠就足够了吗? 我在***.com/questions/9409138/… 的回答正好解决了这个问题。 啊,是的,如果没有 f0g0 采取正确的形式,您将无能为力。我模糊地设想它们都在l 上进行迭代,同时编辑一个总结来自l 的一些信息的向量,可能都是根据Data.Vector.accum 定义的,不确定Data.Vector 可以融合两个accum 调用。跨度> 【参考方案1】:

如果您想确定,唯一的方法就是自己动手。对于任何给定的编译器版本,您可以尝试几种源代码公式并检查生成的核心/程序集/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 版本,编译器可能能够合并abc 的计算,以便在单次遍历@ 期间并行执行(不是在并发意义上) 987654326@,但就目前而言,我相当确信 GHC 没有,如果 JHC 或 UHC 有,我会感到惊讶。为此计算bc 的结构需要足够简单。

在编译器和编译器版本之间以可移植方式获得所需结果的唯一方法是自己动手。至少在接下来的几年里。

取决于f0g0,它可能就像使用适当的累加器类型和组合函数进行严格的左折叠一样简单,就像著名的平均值一样

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 可以帮助避免不必要的共享。

【讨论】:

啊,关于左右折叠的有趣点!不过,我对您的 CSE 点有点困惑。当您尝试天真地编写代码时,您是否只是观察到 CSE 会产生此问题? 如果重新创建列表比存储它更便宜,你会写例如f0 (map h [1 .. n])g0 (map h [1 .. n])。但是编译器可能会消除公共子表达式map h [1 .. n] 并在计算之间共享它。如果不希望发生这种情况,则阻止它并不像相反的那样简单,共享子表达式(如果将其绑定到名称where l = map h [1 .. n] 则完成)。基本上,是的,CSE 可以引入这个问题,而且可能更难解决。

以上是关于在 Haskell 中优化部分计算的主要内容,如果未能解决你的问题,请参考以下文章

如何优化这个 Haskell 程序?

在 Haskell 中优化基数排序

如何避免Haskell空间泄漏? [关闭]

编译器优化后如何分析 Haskell?

如何优化 Haskell 中软实时应用程序的垃圾收集?

如何在Haskell中优化数值库的速度