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上的其他与分析相关的帖子来解释分析数据。
-fllvm
子函数中的41.6%,它决定了填充动态编程的值(&#34) ; DP&#34;)表,(ii)value
函数中的29.1%(生成DP表)和(iii)table
函数中的12.4%,它检查生成的DP表以使确保给定的金额只能以一种方式计算(即,来自一个子组)。rule1
函数相对于value
和table
函数花费了更多时间,因为它是唯一的一个三个没有通过大量元素构建数组或过滤器(它实际上只执行O(1)查找并在rule1
类型之间进行比较,这是您认为的会比较快。)所以这是一个潜在的问题领域。也就是说,Int
函数不太可能推动高堆分配坦率地说,我不确定如何制作三张图表。
堆配置文件图表(即下面的第一个字符):
value
的红色区域代表什么。有意义的是Pinned
函数有一个&#34;尖刻的&#34;内存分配,因为每当dynamic
函数生成满足前三个条件的元组时调用它,并且每次调用它时,它都会创建一个相当大的DP数组。另外,我认为存储元组的内存分配(由构造生成)在程序过程中不会持平。按类型分配和按构造函数分配:
construct
(根据GHC文档表示ByteString或未装箱的数组)表示DP阵列构造的低级执行(在ARR_WORDS
函数中)。坚果我不是百分百肯定。table
和FROZEN
指针类别对应的是什么。不用多说,这里有代码,其中的注释解释了我的算法。我已经尝试确保代码不会从代码框的右侧运行 - 但有些注释确实需要滚动(抱歉)。
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
堆配置文件:
按类型分配:
由构造函数分配:
答案 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语言编写的方式, 赢得每个类别。
列表推导版本更清晰,更易于组合,但性能也不如直接迭代。