【发布时间】:2015-02-16 22:20:42
【问题描述】:
TL;DR:基于 Haskell 代码及其下面的相关分析数据,我们可以得出什么结论让我们修改/改进它,从而缩小与相同算法的性能差距用命令式语言编写(即 C++/Python/C#,但具体语言不重要)?
背景
我编写了以下代码来回答一个热门网站上的问题,该网站包含许多编程和/或数学性质的问题。 (您可能听说过这个网站,有些人将其名称发音为“oiler”,其他人将其发音为“yoolurr”。)由于下面的代码是解决其中一个问题的方法,因此我有意避免提及该网站的名称或问题中的任何特定术语。也就是说,我说的是问题一百零三。
(事实上,我在网站的论坛中看到了很多来自 Haskell 常驻向导的解决方案:P)
我为什么选择分析这段代码?
这是第一个问题(在上述网站上),我遇到了 Haskell 代码与 C++/Python/C# 代码(当两者都使用类似算法时)之间的性能差异(以执行时间衡量)。事实上,对于所有问题(到目前为止,我已经完成了大约 100 个问题,但不是按顺序完成的),优化的 Haskell 代码几乎与最快的 C++ 解决方案并驾齐驱,在其他条件不变的情况下当然是算法。
但是,论坛中针对此特定问题的帖子表明,这些其他语言中的相同算法通常最多需要 1 或 2 秒,最长需要 10-15 秒(假设起始参数相同;我忽略需要 2-3 分钟 + 的非常幼稚的算法。相比之下,下面的 Haskell 代码在我的(体面的)计算机上需要大约 50 秒(禁用分析;启用分析,大约需要 2 分钟,如下所示;注意:使用 @987654327 编译时执行时间相同@)。规格:i5 2.4ghz 笔记本电脑,8gb 内存。
为了努力学习 Haskell,使其成为命令式语言的可行替代品,我解决这些问题的一个目标是学习编写代码,使其性能尽可能与那些命令式语言。在这种情况下,我仍然认为这个问题还没有被我解决(因为性能差异将近 25 倍!)
到目前为止我做了什么?
除了简化代码本身的明显步骤(尽我所能),我还执行了“Real World Haskell”中推荐的标准分析练习。
但我很难得出结论来告诉我哪些部分需要修改。这就是我希望人们能够提供一些指导的地方。
问题描述:
我建议您访问上述网站上问题 103 的网站,但这里有一个简短的总结:目标是找到一个由七个数字组成的组,使得(该组的)任何两个不相交的子组满足以下两个属性(出于上述原因,我试图避免使用“设置”一词...):
- 没有两个子组的总和相同
- 元素较多的子组总和较大(也就是说,最小的四个元素之和必须大于最大的三个元素之和)。
特别是,我们试图找到总和最小的七个数字的组。
我的(当然很弱)观察
警告:其中一些 cmets 很可能完全错误,但我想至少根据我在 Real World Haskell 和 SO 上的其他分析相关帖子中阅读的内容来解释分析数据。
- 确实存在效率问题,因为有三分之一的时间用于垃圾收集 (37.1%)。第一个图表显示在堆中分配了 ~172gb,这看起来很可怕......(也许有更好的结构/函数可用于实现动态编程解决方案?)
- 毫不奇怪,绝大多数 (83.1%) 时间都花在检查规则 1 上:(i) 41.6% 在
value子函数中,它确定要填写动态规划 ("DP") 表的值, (ii)table函数中的 29.1%,它生成 DP 表和 (iii)rule1函数中的 12.4%,它检查生成的 DP 表以确保给定的和只能在一个中计算方式(即,来自一个子组)。 - 但是,我确实感到惊讶的是,与
table和rule1函数相比,value函数花费了更多时间,因为它是三个函数中唯一一个不构造数组或过滤器的函数通过大量元素(实际上只执行 O(1) 查找并在Int类型之间进行比较,您认为这会相对较快)。所以这是一个潜在的问题领域。也就是说,value函数不太可能导致高堆分配
坦率地说,我不确定这三个图表是什么。
堆剖面图(即下面的第一个字符):
- 老实说,我不确定标记为
Pinned的红色区域代表什么。dynamic函数具有“尖峰”内存分配是有道理的,因为每次construct函数生成满足前三个条件的元组时都会调用它,并且每次调用它都会创建一个相当大的 DP 数组。此外,我认为存储元组(由构造生成)的内存分配在程序过程中不会是平坦的。 - 等待澄清“固定”红色区域,我不确定这是否告诉我们任何有用的信息。
按类型分配和按构造分配:
- 我怀疑
ARR_WORDS(根据 GHC 文档表示 ByteString 或未装箱的数组)表示 DP 数组构造的低级执行(在table函数中)。只是,我不能 100% 确定。 - 我不确定
FROZEN和STATIC指针类别对应的是什么。 - 就像我说的那样,我真的不知道如何解释图表,因为(对我而言)没有任何事情出乎意料。
代码和分析结果
事不宜迟,下面是代码,cmets 解释了我的算法。我已尝试确保代码不会超出代码框的右侧 - 但某些 cmets 确实需要滚动(抱歉)。
{-# 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
每个成本中心花费的时间统计:
Wed Dec 17 23:21 2014 Time and Allocation Profiling Report (Final)
103_specialsubset2 +RTS -p -sstderr -RTS
total time = 15.56 secs (15565 ticks @ 1000 us, 1 processor)
total alloc = 118,221,354,488 bytes (excludes profiling overheads)
COST CENTRE MODULE %time %alloc
dynamic.value Main 41.6 17.7
dynamic.table Main 29.1 37.8
construct Main 12.9 37.4
rule1 Main 12.4 7.0
dynamic.table.x Main 1.9 0.0
individual inherited
COST CENTRE MODULE no. entries %time %alloc %time %alloc
MAIN MAIN 55 0 0.0 0.0 100.0 100.0
main Main 111 0 0.0 0.0 0.0 0.0
CAF:main1 Main 108 0 0.0 0.0 0.0 0.0
main Main 110 1 0.0 0.0 0.0 0.0
CAF:main2 Main 107 0 0.0 0.0 0.0 0.0
main Main 112 0 0.0 0.0 0.0 0.0
CAF:main3 Main 106 0 0.0 0.0 0.0 0.0
main Main 113 0 0.0 0.0 0.0 0.0
CAF:construct Main 105 0 0.0 0.0 100.0 100.0
construct Main 114 1 0.6 0.0 100.0 100.0
construct Main 115 1 12.9 37.4 99.4 100.0
rule1 Main 123 282235 0.6 0.0 86.5 62.6
rule1 Main 124 282235 12.4 7.0 85.9 62.6
dynamic Main 125 282235 0.2 0.0 73.5 55.6
dynamic.elements Main 133 282235 0.3 0.1 0.3 0.1
dynamic.len Main 129 282235 0.0 0.0 0.0 0.0
dynamic.table Main 128 282235 29.1 37.8 72.9 55.5
dynamic.table.x Main 130 133204473 1.9 0.0 43.8 17.7
dynamic.value Main 131 133204473 41.6 17.7 41.9 17.7
dynamic.value.m Main 132 132640003 0.3 0.0 0.3 0.0
dynamic.maxSum Main 127 282235 0.0 0.0 0.0 0.0
dynamic.(...) Main 126 282235 0.1 0.0 0.1 0.0
dynamic Main 122 282235 0.0 0.0 0.0 0.0
construct.nn Main 121 12683926 0.0 0.0 0.0 0.0
CAF:main4 Main 102 0 0.0 0.0 0.0 0.0
construct Main 116 0 0.0 0.0 0.0 0.0
construct Main 117 0 0.0 0.0 0.0 0.0
CAF:cap Main 101 0 0.0 0.0 0.0 0.0
cap Main 119 1 0.0 0.0 0.0 0.0
CAF:flr Main 100 0 0.0 0.0 0.0 0.0
flr Main 118 1 0.0 0.0 0.0 0.0
CAF:step_r1dD Main 99 0 0.0 0.0 0.0 0.0
step Main 120 1 0.0 0.0 0.0 0.0
CAF GHC.IO.Handle.FD 96 0 0.0 0.0 0.0 0.0
CAF GHC.Conc.Signal 93 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Encoding 91 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Encoding.Iconv 82 0 0.0 0.0 0.0 0.0
堆配置文件:
按类型分配:
构造函数分配:
【问题讨论】:
-
其他语言的典型运行时间是多少?
-
另外,您是否有其他语言的实现进行比较?
-
我真的很讨厌发布其他人的代码...但是 (i) 鉴于在不查看代码的情况下很难在实现之间进行真正的比较 (ii) 鉴于我已尝试发布此帖子对于解决方案搜索者来说,尽可能地无法搜索,我觉得暂时把它们放上去(我可能会在一段时间后把它们取下来)。 (回复:不完整的 Python 示例 - 我观察到人们通常对执行时间很诚实......)codepad.org/QAD3uEHH(C++,2 秒),codepad.org/1lFxEEyp(不完整的 Python,0.3 秒),codepad.org/VGxsFQeR(C#, 10s)
-
真的希望我不会因为发布这些代码示例而被禁止访问相关网站(半开玩笑),但这是为了学习......无论如何,我一定会把它们删除不久;他们不会无限期地上升......
-
附注接下来的两天我都在路上,但会尝试回答任何澄清问题。如果我不能在周末之前做到这一点,请提前道歉!