我正在比较两个运行相同计算的haskell程序的性能。
第一个是顺序的:
main :: IO()
main = putStr $ unlines . map (show . solve) $ [100..107]
where solve x = pow x (10^7) (982451653)
第二个使用Control.Parallel.Strategies:
import Control.Parallel.Strategies
main :: IO()
main = putStr $ unlines . parMap rdeepseq (show . solve) $ [100..107]
where solve x = pow x (10^7) (982451653)
在两种情况下,pow
是modular exponentiation天真地实现为:
pow :: Int -> Int -> Int -> Int
pow a 0 m = 1
pow a b m = a * (pow a (b-1) m) `mod` m
使用预期的100%CPU,顺序程序在大约3秒钟内运行。
$ stack ghc seq.hs -- -O2
$ \time -f "%e s - %P" ./seq > /dev/null
2.96 s - 100%
如果将并行程序限制为单个内核,则还可以在100%CPU上运行3秒钟。
$ stack ghc par.hs -- -O2 -threaded
$ \time -f "%e s - %P" ./par +RTS -N1 > /dev/null
3.14 s - 99%
但是当我在4个内核上运行它时,我没有观察到预期的性能提升:
$ \time -f "%e s - %P" ./par +RTS -N4 > /dev/null
3.31 s - 235%
更令人惊讶的是,当在多个内核上运行时,顺序程序使用100%以上的CPU:
$ stack ghc seq.hs -- -O2 -threaded
$ \time -f "%e s - %P" ./seq +RTS -N4 > /dev/null
3.26 s - 232%
如何解释这些结果?
编辑-根据@RobertK和@Yuras的建议,我将rdeeseq
替换为rpar
,它确实解决了最初的问题。但是,性能仍然远远低于我的预期:
$ stack ghc par.hs -- -O2 -threaded
$ \time -f "%e s - %P" ./par +RTS -N1 > /dev/null
3.12 s - 99%
$ \time -f "%e s - %P" ./par +RTS -N4 > /dev/null
1.91 s - 368%
即使4个内核平均运行时间超过90%,执行时间也几乎不除以2。
答案 0 :(得分:2)
首先,<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@mipmap/ic_notification" />
似乎是buggy。尝试运行rdeepseq
,您将看不到火花。这就是为什么您看不到4核有任何加速的原因。请改用./seq +RTS -N4 -s
。
还要注意rnf x ‘pseq‘ return x
输出中的GC静力学。实际上,GC占用了大部分CPU。使用+RTS -s
,您将运行4个并行GC,它们将花费更多时间。这就是为什么顺序编程在4个内核上占用更多CPU的原因。基本上,您有3个GC线程在旋转锁中处于空闲状态,等待同步。无用的事情,是在繁忙的循环中吃掉CPU。尝试使用-N4
选项限制并行GC线程的数量。
关于性能提升。您不应期望完美的缩放比例。另外,我认为您有1个起泡的火花-并行评估,但未使用其结果。
添加:与您在注释中链接的python实现相比,我发现您在haskell中使用了完全不同的算法。下一个或多或少类似的方法(要求-qn1
):
BangPatterns
您的原始算法使用堆栈来构建结果,因此它受GC约束,而不是受实际计算约束。因此,您不会看到很大的加速。在使用新算法时,我看到了3倍的加速(我不得不增加工作量才能看到加速,因为算法变得太慢了。)
答案 1 :(得分:1)
我不认为您的并行示例是并行的。 parMap
接受一个策略,您的策略只是告诉它执行一个deepseq。您需要将此策略与定义并行行为的策略相结合,例如rpar
。您正在告诉haskell“使用此策略执行此地图”,现在您的策略未定义任何并行行为。
还要确保您在编译程序时指定-rtsopts
标志(我不知道stack是否为您完成此操作,但是ghc要求它启用运行时选项)。