【问题标题】:Optimize a list function that creates too much garbage (not stack overflow)优化一个产生过多垃圾的列表函数(不是堆栈溢出)
【发布时间】:2015-09-24 15:29:36
【问题描述】:

我有那个 Haskell 函数,这导致我的程序的所有分配超过 50%,导致 GC 占用了我 60% 的运行时间。我使用一个小堆栈 (-K10K) 运行,所以没有堆栈溢出,但我可以让这个函数更快,分配更少吗?

这里的目标是计算矩阵与向量的乘积。例如,我不能使用hmatrix,因为这是使用ad Automatic Differentiation 包的更大功能的一部分,所以我需要使用Num 的列表。在运行时,我想Numeric.AD 模块的使用意味着我的类型必须是Scalar Double

listMProd :: (Num a) => [a] -> [a] -> [a]
listMProd mdt vdt = go mdt vdt 0
  where
    go [] _  s = [s]
    go ls [] s = s : go ls vdt 0
    go (y:ys) (x:xs) ix = go ys xs (y*x+ix)

基本上,我们循环遍历矩阵,将累加器相乘和相加,直到到达向量的末尾,存储结果,然后再次重新启动向量。我有一个quickcheck 测试,验证我得到的结果与 hmatrix 中的矩阵/向量积相同。

我已经尝试过使用foldlfoldr 等。我尝试过的任何方法都没有使函数更快(并且像foldr 这样的一些东西会导致空间泄漏)。

运行分析告诉我,除了这个函数是花费大部分时间和分配的地方之外,还有大量 Cells 正在创建,Cells 是来自 @987654341 的数据类型@包。

运行一个简单的测试:

import Numeric.AD

main = do
    let m :: [Double] = replicate 400 0.2
        v :: [Double] = replicate 4 0.1
        mycost v m = sum $ listMProd m v 
        mygrads = gradientDescent (mycost (map auto v)) (map auto m)
    print $ mygrads !! 1000

这在我的机器上告诉我 GC 有 47% 的时间都在忙。

有什么想法吗?

【问题讨论】:

  • 更多信息!你是如何运行这个程序的?你的测试工具在哪里?您使用的是什么具体类型? Haskell 编译器的标志和版本是什么?
  • 添加了一些信息。该函数通过使用自己的类型(Num 实例)的 ad grad 函数调用。分析显示“单元”的分配。
  • 一些半知半解的建议:您是否考虑过在ST 中使用可变状态?还有stream-fusion/conduit/pipes?也许(?)将您的矢量列表转换为其他东西甚至是值得的,例如unboxed vector?我没有任何这些技术的经验,但也许这些链接可以帮助你进一步。
  • 只是为了排除一个明显的事情:如果你在go 的最后一个子句中添加一个爆炸模式,例如go (y:ys) (x:xs) !ix = go ys xs (y*x+ix)
  • 你能给出一个最小的可执行示例吗?

标签: performance list haskell garbage-collection automatic-differentiation


【解决方案1】:

一个非常简单的优化是通过其累加器参数使go 函数严格,因为它很小,如果a 是原始的并且总是需要完全评估,则可以取消装箱:

{-# LANGUAGE BangPatterns #-}
listMProd :: (Num a) => [a] -> [a] -> [a]
listMProd mdt vdt = go mdt vdt 0
  where
    go [] _  !s = [s]
    go ls [] !s = s : go ls vdt 0
    go (y:ys) (x:xs) !ix = go ys xs (y*x+ix)

在我的机器上,它提供了 3-4 倍的加速(使用 -O2 编译)。

另一方面,中间列表不应该是严格的,所以它们可以被融合。

【讨论】:

  • 嗯,好主意,但在我的用例中根本没有帮助(速度或 GC 使用率没有提高)。我认为通过广告库调用函数这一事实会影响性能(我看到具有严格 Int 字段的 Cells 数据类型)。
最近更新 更多