【问题标题】:Sorting in parallel performance并行性能排序
【发布时间】:2020-06-17 22:28:21
【问题描述】:

我尝试用多核运行一些程序,结果有点困惑。 默认情况下,在下面的程序中排序需要 20 秒,当我使用 +RTS -N2 运行它时大约需要 16 秒,但使用 +RTS -N4 需要 21 秒!

为什么会这样?并且有没有每个额外内核变得更快的程序示例? (与教程中的其他程序有相似的结果)

以下是程序示例:

import Data.List
import Control.Parallel
import Data.Time.Clock.POSIX

qsort :: Ord a => [a] -> [a]
qsort (x:xs)
  = let a = qsort $ filter (<=x) xs
        b = qsort $ filter (>x)  xs
    in b `par` a ++ x:b
qsort [] = []


randomList :: Int -> [Int]
randomList n = take n $ tail (iterate lcg 1)
  where lcg x = (a * x + c) `rem` m
        a = 1664525
        c = 1013904223
        m = 2^32

main :: IO ()
main = do
  let randints = randomList 5000000
  t1 <- getPOSIXTime
  print . sum $ qsort randints
  t2 <- getPOSIXTime
  putStrLn $ "SORT TIME: " ++ show (t2 - t1) ++ "\n"

【问题讨论】:

  • 大问题。首先,懒惰使了解并行实际发生的事情变得有点棘手。您实际上并没有像您想象的那样并行执行。
  • 您在时间测量中包括列表生成。您是否尝试过在启动计时器之前强制列表?
  • 你也可以尝试添加-A1000M,看看垃圾回收是否有很大的不同。也可以推荐一下threadscope程序,可以用来可视化。
  • @FyodorSoikin 我在列表中尝试了 deepseq,它减少了时间,但相互比较结果是相同的
  • @dfeuer 你能给我一个包含数组输入和输出的代码的链接吗?我很乐意尝试一下。您链接到的答案非常好,无论优化如何,它都永远不会是最佳的。我也不明白问这些问题的人。例如,我给了@crends 链接到我在 Haskell 中看到的最快的快速排序实现,它自然使用所有内核,我回复:“我不仅对性能感兴趣,而且对并行化感兴趣。”答案我尽管我提供的解决方案比接受的答案快 x1000,但未接受链接到。

标签: haskell


【解决方案1】:

我无法复制您的结果。 (这是一件好事,因为我认为我是声称使用您发布的代码使用 -N2-N4 看到性能改进的人。)

在使用 GHC 8.8.3 的 Linux 上,并使用 -O2 -threaded 编译为独立的可执行文件,我在 4 核桌面上得到以下时间:

$ stack ghc -- --version
Stack has not been tested with GHC versions above 8.6, and using 8.8.3, this may fail
Stack has not been tested with Cabal versions above 2.4, but version 3.0.1.0 was found, this may fail
The Glorious Glasgow Haskell Compilation System, version 8.8.3
$ stack ghc -- -O2 -threaded QuickSort3.hs
Stack has not been tested with GHC versions above 8.6, and using 8.8.3, this may fail
Stack has not been tested with Cabal versions above 2.4, but version 3.0.1.0 was found, this may fail
[1 of 1] Compiling Main             ( QuickSort3.hs, QuickSort3.o )
Linking QuickSort3 ...
$ ./QuickSort3 +RTS -N1
10741167410134688
SORT TIME: 7.671760902s

$ ./QuickSort3 +RTS -N2
10741167410134688
SORT TIME: 5.700858877s

$ ./QuickSort3 +RTS -N3
10741167410134688
SORT TIME: 4.88330669s

$ ./QuickSort3 +RTS -N4
10741167410134688
SORT TIME: 4.93364958s

我在 16 核 Linux 笔记本电脑上得到了类似的结果,在该笔记本电脑上运行 4 核 Windows 虚拟机(也使用 GHC 8.8.3)也得到了类似的结果。

我可以为您的结果想出一些可能的解释。

首先,我没有一台速度非常快的台式机,所以你的 20 秒时间似乎很可疑。您是否可能正在做类似的事情:

$ stack runghc QuickSort3.hs +RTS -N4

如果是这样,这会将+RTS 标志传递给stack,然后使用慢速字节码解释器以单线程模式运行Haskell 程序。在我的测试中,无论我通过什么 -Nx 标志值,排序都需要大约 30 秒。

其次,您是否有可能在内核数量有限的虚拟机上运行此程序(或非常旧的两核硬件)?如前所述,我尝试在 Windows 虚拟机下进行测试,得到的结果与使用 4 核虚拟机的 Linux 版本相似,但使用 2 核虚拟机的结果非常不稳定(例如,@987654330 的 11.4、13.0 和 51.3 秒) @、-N2-N4,因此一般来说,更多内核的性能较差,而 4 内核的性能非常差)。

您可以尝试以下简单的并行求和基准,它可能会更好地扩展:

import Data.List
import Control.Parallel
import Data.Time.Clock.POSIX

randomList :: Int -> Int -> [Int]
randomList seed n = take n $ tail (iterate lcg seed)
  where lcg x = (a * x + c) `rem` m
        a = 1664525
        c = 1013904223
        m = 2^32

main :: IO ()
main = do
  t1 <- getPOSIXTime
  let n = 50000000
      a = sum $ randomList 1 n
      b = sum $ randomList 2 n
      c = sum $ randomList 3 n
      d = sum $ randomList 4 n
      e = sum $ randomList 5 n
      f = sum $ randomList 6 n
      g = sum $ randomList 7 n
      h = sum $ randomList 8 n
  print $ a `par` b `par` c `par` d `par` e `par` f `par` g `par` h `par` (a+b+c+d+e+f+g+h)
  t2 <- getPOSIXTime
  putStrLn $ "SORT TIME: " ++ show (t2 - t1) ++ "\n"

【讨论】:

  • 我在没有使用堆栈的情况下进行了测试。我的笔记本电脑(不是虚拟机)上同时安装了 W10 和 ubuntu。我的处理器是英特尔酷睿 i5-4200U。您的代码对我有用,结果从 -N1 到 -N4:16s、10.5s、9.4s、8.7s。我在 Haskell 上的表现真的很奇怪,我尝试用 C++ 和 Python 解决相同的任务,甚至 Python 的排序速度也快了 10 倍!
  • 啊,好吧,Core i5-4200U 在技术上是一个 2 核处理器,尽管它支持多达 4 个线程的超线程,所以这可以解释你的结果。 (我的桌面是一个超频的 i5-6600K,它已经老了,但有 4 个完整的内核。)我不确定为什么快速排序在 -N4 上的性能更差,而并行和性能更好,但快速排序示例进行了大量的垃圾收集,并且我认为当线程数超过真实内核数时并行GC表现特别差。
  • @k-a-buhr 谢谢。你能说我是否应该总是用-O2编译吗?我尝试从 Data.Vector 对与向量相同的数据进行排序,它实际上比没有 -O2 的列表排序慢(没有 -O2 的 20 秒和使用 -O2 的 3.5 秒)
  • 是的,我认为-O2 被认为是生产代码和基准测试的标准优化级别。使用-O0(默认值)的唯一原因是它在开发过程中更快(并且在小的更改后重新编译大型多模块程序时可能更快,因为-O2 比@987654338 进行更多的跨模块重新编译@)。不知道有没有人用-O1(相当于-O没有号码);如果-O0 生成的代码运行速度太慢,可能对开发有帮助。
  • @k-a-buhr 您能否在这里解释一下您的回答中的火花数量限制:stackoverflow.com/a/55894549/13765493? 20层不是说最多有1kk火花吗? (而且已经太多了?)通过检查子列表长度来限制火花创建不是更好吗?
猜你喜欢
  • 2012-01-16
  • 2012-08-22
  • 2014-07-30
  • 2019-03-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多