使用GHC的分析统计数据/图表来识别故障区域/提高Haskell代码的性能

时间:2014-12-18 07:32:53

标签: haskell profiling

TL; DR:根据下面的Haskell代码及其相关的分析数据,我们可以得出哪些结论让我们修改/改进它,以便缩小性能差距vs用命令式语言编写的相同算法(即C ++ / Python / C#但特定语言并不重要)?

背景

我编写了以下代码作为对热门网站上的问题的回答,该网站包含许多编程和/或数学性质的问题。 (您可能已经听说过这个网站,其名称发音为#34; oiler"有些人," yoolurr"其他人。)因为下面的代码是其中一个的解决方案。问题,我故意避免提及网站的名称或问题中的任何具体条款。也就是说,我正在谈论问题一百零三。

(事实上,我在常驻Haskell向导的网站论坛中看到了很多解决方案:P)

为什么我选择分析此代码?

这是第一个问题(在所说的网站上),我遇到了Haskell代码与C ++ / Python / C#代码之间的性能差异(以执行时间衡量)(当两者都使用类似的算法时)。事实上,所有这些问题(迄今为止;我已经完成了约100个问题但不是顺序问题)的情况就是优化的Haskell代码与最快的C ++解决方案并驾齐驱,其他条件不变当然,对于算法来说。

然而,论坛中针对此特定问题的帖子表明,这些其他语言中的相同算法通常最多需要一到两秒,最长需要10-15秒(假设相同的起始参数; I&# 39; m忽略了需要2-3分钟的非常天真的算法+)。相比之下,下面的Haskell代码在我的(正常)计算机上需要约50秒(禁用分析;启用分析后,需要约2分钟,如下所示;注意:使用{{编译时,执行时间相同1}})。规格:i5 2.4ghz笔记本电脑,8GB RAM。

为了努力学习Haskell,它可以成为命令式语言的可行替代品,我解决这些问题的目的之一就是学习编写尽可能具有“性能”的代码。与那些命令式语言相提并论。在这种情况下,我仍然认为这个问题尚未解决(因为性能差异大约为25倍!)

到目前为止我做了什么?

除了简化代码本身(尽我所能)的明显步骤之外,我还执行了标准的分析练习,这些练习是在#34; Real World Haskell&#34;。< / p>

但是我很难得出结论,告诉我需要修改哪些部分。我希望大家可以帮助提供一些指导。

问题描述:

我在上述网站上向您推荐问题一百零一的网站,但这里有一个简短的总结:目标是找到一组七个数字,这样任何两个不相交的子组(该群体)满足以下两个属性(我试图避免因上述原因而使用&#39; se-t&#39;):

  • 没有两个小组总和相同的金额
  • 具有更多元素的子组具有更大的总和(换句话说,最小的四个元素的总和必须大于最大的三个元素的总和)。

特别是,我们试图找到总数最小的七个数字组。

我的(无可否认的弱)观察

警告:其中一些评论可能完全错误,但我想至少根据我在Real World Haskell上阅读的内容以及SO上的其他与分析相关的帖子来解释分析数据。

  • 确实似乎存在效率问题,因为三分之一的时间用于垃圾收集(37.1%)。第一个数据表显示堆中分配了~172gb,这看起来很糟糕......(也许有更好的结构/功能用于实现动态编程解决方案?)
  • 毫不奇怪,绝大多数(83.1%)的时间用于检查规则1:(i)-fllvm子函数中的41.6%,它决定了填充动态编程的值(&#34) ; DP&#34;)表,(ii)value函数中的29.1%(生成DP表)和(iii)table函数中的12.4%,它检查生成的DP表以使确保给定的金额只能以一种方式计算(即,来自一个子组)。
  • 但是,我确实发现,rule1函数相对于valuetable函数花费了更多时间,因为它是唯一的一个三个没有通过大量元素构建数组或过滤器(它实际上只执行O(1)查找并在rule1类型之间进行比较,这是您认为的会比较快。)所以这是一个潜在的问题领域。也就是说,Int函数不太可能推动高堆分配

坦率地说,我不确定如何制作三张图表。

堆配置文件图表(即下面的第一个字符):

  • 老实说,我不确定标记为value的红色区域代表什么。有意义的是Pinned函数有一个&#34;尖刻的&#34;内存分配,因为每当dynamic函数生成满足前三个条件的元组时调用它,并且每次调用它时,它都会创建一个相当大的DP数组。另外,我认为存储元组的内存分配(由构造生成)在程序过程中不会持平。
  • 有待澄清&#34;固定&#34;红色区域,我不确定这个告诉我们任何有用的东西。

按类型分配和按构造函数分配:

  • 我怀疑construct(根据GHC文档表示ByteString或未装箱的数组)表示DP阵列构造的低级执行(在ARR_WORDS函数中)。坚果我不是百分百肯定。
  • 我不确定tableFROZEN指针类别对应的是什么。
  • 就像我说的那样,我真的不确定如何解读图表,因为没有任何事情像我一样跳出来(对我而言)。

代码和分析结果

不用多说,这里有代码,其中的注释解释了我的算法。我已经尝试确保代码不会从代码框的右侧运行 - 但有些注释确实需要滚动(抱歉)。

STATIC

堆分配,垃圾收集和已用时间的统计信息:

{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -Wall #-}

import CorePrelude
import Data.Array
import Data.List
import Data.Bool.HT ((?:))
import Control.Monad (guard)

main = print (minimum construct)

cap = 55 :: Int
flr = 20 :: Int
step = 1 :: Int

--we enumerate tuples that are potentially valid and then
--filter for valid ones; we perform the most computationally
--expensive step (i.e., rule 1) at the very end
construct :: [[Int]]
construct = {-# SCC "construct" #-} do
  a <- [flr..cap]                         --1st: we construct potentially valid tuples while applying a
  b <- [a+step..cap]                      --constraint on the upper bound of any element as implied by rule 2
  c <- [b+step..a+b-1]
  d <- [c+step..a+b-1]
  e <- [d+step..a+b-1]
  f <- [e+step..a+b-1]
  g <- [f+step..a+b-1]
  guard (a + b + c + d - e - f - g > 0)   --2nd: we screen for tuples that completely conform to rule 2
  let nn = [g,f,e,d,c,b,a]
  guard (sum nn < 285)                    --3rd: we screen for tuples of a certain size (a guess to speed things up)
  guard (rule1 nn)                        --4th: we screen for tuples that conform to rule 1
  return nn

rule1 :: [Int] -> Bool
rule1 nn = {-# SCC "rule1" #-} 
    null . filter ((>1) . snd)           --confirm that there's only one subgroup that sums to any given sum
  . filter ((length nn==) . snd . fst)   --the last column us how many subgroups sum to a given sum
  . assocs                               --run the dynamic programming algorithm and generate a table
  $ dynamic nn

dynamic :: [Int] -> Array (Int,Int) Int
dynamic ns = {-# SCC "dynamic" #-} table
  where
    (len, maxSum) = (length &&& sum) ns
    table = array ((0,0),(maxSum,len)) 
      [ ((s,i),x) | s <- [0..maxSum], i <- [0..len], let x = value (s,i) ]
    elements = listArray (0,len) (0:ns)
    value (s,i)
      | i == 0 || s == 0 = 0
      | s ==  m = table ! (s,i-1) + 1
      | s > m = s <= sum (take i ns) ?: 
          (table ! (s,i-1) + table ! ((s-m),i-1), 0)
      | otherwise = 0
      where
        m = elements ! i

每个成本中心花费的统计时间:

% ghc -O2 --make 103_specialsubset2.hs -rtsopts -prof -auto-all -caf-all -fforce-recomp
[1 of 1] Compiling Main             ( 103_specialsubset2.hs, 103_specialsubset2.o )
Linking 103_specialsubset2 ...
% time ./103_specialsubset2.hs +RTS -p -sstderr
zsh: permission denied: ./103_specialsubset2.hs
./103_specialsubset2.hs +RTS -p -sstderr  0.00s user 0.00s system 86% cpu 0.002 total
% time ./103_specialsubset2 +RTS -p -sstderr
SOLUTION REDACTED
 172,449,596,840 bytes allocated in the heap
  21,738,677,624 bytes copied during GC
         261,128 bytes maximum residency (74 sample(s))
          55,464 bytes maximum slop
               2 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0     327548 colls,     0 par   27.34s   41.64s     0.0001s    0.0092s
  Gen  1        74 colls,     0 par    0.02s    0.02s     0.0003s    0.0013s

  INIT    time    0.00s  (  0.01s elapsed)
  MUT     time   53.91s  ( 70.60s elapsed)
  GC      time   27.35s  ( 41.66s elapsed)
  RP      time    0.00s  (  0.00s elapsed)
  PROF    time    0.00s  (  0.00s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time   81.26s  (112.27s elapsed)

  %GC     time      33.7%  (37.1% elapsed)

  Alloc rate    3,199,123,974 bytes per MUT second

  Productivity  66.3% of total user, 48.0% of total elapsed

./103_specialsubset2 +RTS -p -sstderr  81.26s user 30.90s system 99% cpu 1:52.29 total

堆配置文件:

heap profile

按类型分配:

enter image description here

由构造函数分配:

TBD

1 个答案:

答案 0 :(得分:2)

有很多可以说的。在这个答案中,我将只评论construct函数中的嵌套列表推导。

为了了解construct中发生了什么,我们将其隔离并将其与您用命令式语言编写的嵌套循环版本进行比较。我们删除了rule1后卫以仅测试列表的生成。

-- List.hs -- using list comprehensions

import Control.Monad

cap = 55 :: Int
flr = 20 :: Int
step = 1 :: Int

construct :: [[Int]]
construct =  do
  a <- [flr..cap]                         
  b <- [a+step..cap]                      
  c <- [b+step..a+b-1]
  d <- [c+step..a+b-1]
  e <- [d+step..a+b-1]
  f <- [e+step..a+b-1]
  g <- [f+step..a+b-1]
  guard (a + b + c + d - e - f - g > 0)
  guard (a + b + c + d + e + f + g < 285)
  return  [g,f,e,d,c,b,a]
  -- guard (rule1 nn)

main = do
  forM_ construct print


-- Loops.hs -- using imperative looping

import Control.Monad

loop a b f = go a
  where go i | i > b     = return ()
             | otherwise = do f i; go (i+1)

cap = 55 :: Int
flr = 20 :: Int
step = 1 :: Int

main =
  loop flr cap $ \a ->
  loop (a+step) cap $ \b ->
  loop (b+step) (a+b-1) $ \c ->
  loop (c+step) (a+b-1) $ \d ->
  loop (d+step) (a+b-1) $ \e ->
  loop (e+step) (a+b-1) $ \f ->
  loop (f+step) (a+b-1) $ \g ->
    if (a+b+c+d-e-f-g > 0) && (a+b+c+d+e+f+g < 285)
      then print [g,f,e,d,c,b,a]
      else return ()

这两个程序都使用ghc -O2 -rtsopts进行编译,并使用prog +RTS -s > out运行。

以下是结果摘要:

                          Lists.hs    Loops.hs
  Heap allocation        44,913 MB    2,740 MB
  Max. Residency            44,312      44,312
  %GC                        5.8 %       1.7 %
  Total Time             9.48 secs   1.43 secs

正如您所看到的,循环版本,这是您用C语言编写的方式, 赢得每个类别。

列表推导版本更清晰,更易于组合,但性能也不如直接迭代。