【问题标题】:Haskell List Comprehension Speed InconsistenciesHaskell 列表理解速度不一致
【发布时间】:2014-09-01 15:31:18
【问题描述】:

我正在尝试优化我的程序的执行速度,我遇到了一些有趣的结果,希望有人能回答。似乎对我的一个列表推导进行小改动会极大地改变执行速度,但我不知道为什么。

这是我现在的程序。

import Data.Ord
import Control.Monad
import Data.Array
import Data.Ix
import qualified Data.Map as M
import qualified Data.Set as S
import Data.List (minimumBy, foldl')


arrayMatrix lists = let rlen = length lists
                        clen = length $ head lists
                        r    = ((1,1), (rlen, clen))
                    in array r . zip (range r) $ concat lists

a_star start goal h m = search S.empty (S.singleton start) 
                        (M.singleton start (m ! start)) 
                        $ M.singleton start (m ! start + h ! start)
    where neighbors (r,c) = filter (inRange $ bounds m) [ (r-1,c), (r,c+1), (r+1,c) , (r,c-1)]
          search closed open gs fs
              | S.null open     = 0
              | current == goal = gs M.! goal
              | otherwise       = let open'   = S.delete current open
                                      closed' = S.insert current closed
                                      neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
                                                , let ts = gs M.! current + m ! n ]
                                      actionable = filter (\(n,ts) -> S.notMember n open' || ts < (gs M.! n)) neighbs
                                      (op',gs',fs') = foldl' (\(o,ng,nf) (n,ts) -> (S.insert n o, M.insert n ts ng, M.insert n (ts + h ! n) nf)) (open',gs,fs) actionable
                                  in search closed' op' gs' fs'
              where current = minimumBy (comparing (fs M.!)) $ S.toList open

main = do
  matrix <- liftM (arrayMatrix . map (read . ('[':) . (++"]")) . lines) 
            $ readFile "matrix.txt"
  let bds       = bounds matrix
      ulim      = snd bds
      heuristic = let m   = minimum $ elems matrix
                    in listArray bds . map (\(r,c) -> (uncurry (+) ulim)-r-c) $ range bds
  print $ a_star (1,1) ulim heuristic matrix

现在程序在我的电脑上运行约 350 毫秒(使用 GHC 7.8.2 -O2 编译),matrix.txt 由 Project Euler 提供。

如果我改变邻居

neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
          , let ts = gs M.! current + m ! n ]

neighbs = [(n, gs M.! current + m ! n) | n <- neighbors current, S.notMember n closed]

执行时间增加到超过 1 秒。
其他小的更改,例如将下一行的过滤器移动到列表推导中会产生相同的结果:~1sec。
谁能解释为什么会这样?

编辑:这似乎不会发生在早期版本的 GHC 上。我尝试了 GHC 7.6.3,每一个的表现都差不多。

按照cdk 的建议,我已经包含了运行ghc -O2 -ddump-simpl -dsuppress-all 的转储。我真的不知道我在看什么,所以如果有人能够解释,那将是一个很大的帮助,谢谢。

Link to both dumps

EDIT2(对 Priyatham 的回应):我认为情况并非如此。我变了

neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
          , let ts = gs M.! current + m ! n ]
actionable = filter ((n,ts) -> S.notMember n open' || ts < (gs M.! n)) neighbs

neighbs = [(n, gs M.! current + m ! n) | n <- neighbors current, S.notMember n closed ]
actionable = filter ((n,!ts) -> S.notMember n open' || ts < (gs M.! n)) neighbs

使用 BangPatterns,它仍然运行一秒钟多一点。实际上,从

修改 neigbs
neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
          , let ts = gs M.! current + m ! n ]

neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
          , let !ts = gs M.! current + m ! n ]  -- Added bang before ts

将运行时间也增加到 1 秒以上。

【问题讨论】:

  • 我正在尝试重现它。你能把你的matrix.txt 粘贴到某个地方吗?
  • 啊忘记了,here你去吧。
  • 一个很好的练习是使用ghc -O2 -ddump-simpl -dsuppress-all检查neighbs的每个变体生成的核心GHC
  • 我已将信息包含在原帖中
  • 看看 [this] (haskell.org/haskellwiki/Let_vs._Where) 的最后一点。它似乎与您的示例相似。在这种情况下,问题是每次都会重新计算地图,但我不能确定这是否是这里发生的事情,我也无法解释为什么。

标签: list haskell optimization list-comprehension ghc


【解决方案1】:

以下是关于 let ts =let !ts = 发生了什么的猜测。一世 通过查看-ddump-stranal 的输出得到它(转储 严格性分析注释),并阅读Demand analyser in GHC

let !ts =let ts = 之间的区别在于,如果 ts 是 底部(即undefined),然后n被评估 因为ts 将首先被评估并且评估将停止。它 似乎两个程序之间的区别在于 整数 n 在一个版本中是严格且未装箱的,但在 其他(见-ddump-stranal-ddump-simpl的输出;链接 上面描述了输出)。

!ts 与否!ts 如何影响n 的严格性?我认为如果 ts 是底部然后程序 必须 失败 之前 评估 n 或 它的任何元素(我不确定它是 n :: (Int, Int) 本身还是 它的元素)。所以 ghc 似乎做了正确的事情来保留nts 需要严格时非严格,因为评估n 首先并且可能在不同的地方失败可能是一个错误。

接下来,如何强制!tsn 没有影响?请注意ts 如果gscurrentcurrentn、 或m 已知不是底部(这些是表达式的所有元素,除了n)并且已经被评估(我认为M.!! 可能 不先评估他们的论点,永远不要跌倒)。所以我们需要强加条件“ts是底部暗示 n 是底部并且已经被评估”,因此 ghc 知道首先评估 n 是安全的。

我的解决方案:为currentgsm 添加爆炸模式。用我的 ghc 7.8.2,这似乎解决了问题。似乎也只有 current 需要强制。

我不太确定关于移动表达式的原始问题 ts 进入元组,但相同的解决方案似乎有效。

附:请注意,

filter (\x -> x > 5) [x | x <- [1..10]] == [x | x <- [1..10], x > 5]

所以在您的列表中neighbsactionable 将过滤谓词带入会更干净 列表理解本身是这样的:

[(n, ts)
| n <- neighbors current
, S.notMember n closed
, let ts = gs M.! current + m ! n
, S.notMember n open' || ts < (gs M.! n)
]

【讨论】:

  • 我会将谓词带入列表理解中,但这会导致我试图弄清楚的同样的减速。您在代码中的哪个位置添加了爆炸模式?我在 where 语句中设置了 current strict 并没有看到任何效果(当您引用添加 bangpatterns 的内容时,您的意思是 m 还是 n?)。
  • @dsemi 尝试向neighborssearch 添加类型注释。你是对的,对于你的原始代码,where !current = 没有加速。这很奇怪。我在尝试理解代码时添加了这些类型注释,但我忘记了它们可能会有所作为,似乎确实如此。在性能方面,多态函数比专用函数更差(或相等)。
  • 好笑的是,我最近才遇到这种现象;我在责备自己没有早点尝试这个。添加比推断的多态注释更具体的类型注释极大地提高了性能,不需要爆炸模式。我也能够在列表理解中包含谓词而不会影响性能,谢谢!我对最初的问题仍然很模糊,但是根据这些信息,您是否认为在一种情况下编译器具有更好的类型推断而不是不同的严格性规则?
  • @dsemi 好吧,如果 ghc 无法确定多态函数主要/仅在 int 上调用,它不会为 int 生成专门的代码。或者它可能会生成使用装箱整数而不是原语的代码,这就是我认为它在这里所做的。严格性分析是另一件可能阻止/允许拆箱的事情(因为拆箱的值不能是未定义的)。我认为这是在昂贵的多态函数上使用 SPECIALIZE pragma 的原因之一。
  • TLDR:向您的函数添加类型签名。这是一种很好的做法,可以使代码更具可读性,在某些情况下甚至可以提高性能。
【解决方案2】:

这不是一个完整的答案,因为我缺乏关于如何在内部实现 let 和列表推导的信息。

neighbs 中的每个项目都是一个元组,在 WHNF 中,总和没有被严格评估。这会留下未评估的 thunk,这可能会增加运行时间。

如果可能的话,我建议使用seq 重写第二个定义而不使用let,看看运行时间是否下降(在这种情况下,这个答案很可能是正确的)。

阅读this 了解WHNF 是什么。

【讨论】:

  • 这看起来不像是我的答案。
猜你喜欢
  • 2022-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-10
相关资源
最近更新 更多