【问题标题】:Why is using replicate much slower than serial execution?为什么使用复制比串行执行慢得多?
【发布时间】:2011-04-29 15:24:23
【问题描述】:

我有点问题。我想使用 scala.concurrent.ops.replicate 来并行化我的程序。但我发现,算法实际上变得慢得多。 所以我写了一个小测试,仍然得到相同的结果。所以他们来了。

序列号:大约需要 63 秒才能完成

object SerTest {
  def main(args: Array[String]) {
      for(x <- 1 to 10){
        for(i <- 1 to 4) {
          for(j <- 1 to 100000) {
            val a = BigInt(j).isProbablePrime(1000)
            if(!a && j == 100000) println(i + " is ready")}}}}}

并发代码:大约需要 161 秒才能完成

object ParTest {
  def main(args: Array[String]) {
      for(x <- 1 to 10){
        replicate(1,5) { i =>
          for(j <- 1 to 100000) {
            val a = BigInt(j).isProbablePrime(1000)
            if(!a && j == 100000) println(i + " is ready")}}}}}

那么,我犯的完全明显且令人尴尬的错误在哪里? :)

编辑:哦,我在四核 CPU 上运行它。所以它实际上应该更快:)

Edit2:由于 Kevin Wright 的回答,我稍微更改了程序以延长运行时间。

【问题讨论】:

  • BigInt.isProbalyPrime 中是否发生了一些奇怪的事情?我用一些愚蠢的斐波那契线替换了那条线,复制代码确实更快(在双核上)。
  • 哦,哇,你是对的 :) 永远不会想到这一点。只是想使用一种我认为需要一些时间来计算的方法。尽管为什么会发生这种情况仍然很有趣。因为我在原始程序中得到了相同的行为,并且我没有使用 isProbablePrime 或类似的东西。

标签: scala concurrency scala-2.8


【解决方案1】:

查看您的示例代码,我猜您正在从命令行直接跳转到 main 方法。这是在 Java 中进行微分析的绝对最糟糕的方法!

您应该首先运行您的测试几次(在同一个 VM 调用中),至少足以让 JVM 在您思考之前已经正确预热并运行 30 秒。 em> 关于开始测量任何东西。这将确保它运行的是已编译(而非解释)的代码,并且已经过全面优化。

您还需要了解启动线程的成本。对于短时间运行的循环,这将是一个令人望而却步的开销,并且会比循环本身消耗更多的时间!

更新

以下定义来自 ops.scala:

val defaultRunner: FutureTaskRunner = TaskRunners.threadRunner
def spawn(p: => Unit)(implicit runner: TaskRunner = defaultRunner): Unit = {...}
def replicate(start: Int, end: Int)(p: Int => Unit) {...}

所以实际使用的跑步者是作为隐式注入的, 或默认为TaskRunners.threadRunner

您可以尝试将其更改为使用线程池,方法是在您的代码前加上:

implicit val runner = TaskRunners.threadPoolRunner

或者我相信以下方法也可以:

import concurrent.TaskRunners.threadPoolRunner

看看有没有区别


三思而后行……

我认为该参数实际上不会传递给对spawn 的嵌套调用,如果您自己复制该方法可能会更好(我目前在邮件列表上对此进行了查询)。

为了您的方便,这里是完整的、可怕的、荣耀的方法:

def replicate(start: Int, end: Int)(p: Int => Unit) {
  if (start == end) 
    ()
  else if (start + 1 == end)
    p(start)
  else {
    val mid = (start + end) / 2
    spawn { replicate(start, mid)(p) }
    replicate(mid, end)(p)
  }
}

(你仍然需要定义隐式运行器...)

【讨论】:

  • 好的,我已经做到了,但变化不大。如果我运行外部循环 10 次,串行程序需要 63 秒,并行程序需要 161 秒。关于启动时间:我只启动 4 个线程,因为我只有 4 个核心,并且每个内部循环都需要一秒钟的时间来执行。因此,我认为我正在为程序提供最佳条件,以在理论上在多核上更快。
  • 你跑了 10 次,然后测量了第 11 次?多次运行的结果有多一致?
  • 不,我测量了所有 10 次运行。我知道你要去哪里,因为我仍然有所有的设置和 JIT-Time,如果它有细微的差别,我会同意。但是 63 秒对 161 秒......那里肯定有一个更大的问题。
  • 好的,所以我也分别测试了所有运行,并且在大约 3. 执行之后结果甚至出来了。然后我得到 5.5 秒的串行代码和大约 18 秒的并发代码。有趣的是:从第一个循环开始,并发代码的时间就相当稳定了。那么也许 JIT 没有优化它?
  • 谢谢,但这也不会改变任何事情。与此同时,我还使用了分析器来查看问题,虽然我对这种工具不是很有经验,但在我看来,正在创建的 4 个线程大部分时间都相互阻塞。
【解决方案2】:

查看 BigInteger.isProbablePrime 的源代码(BigInt 委托给 java 库)。它正在做大量的 new BigInteger() 因为那是一个不可变的类。

我的猜测是内存分配导致过多的争用从并行化中受益。您可以通过用一个简单的计算(例如将 100MM 数字相乘)代替您的素数测试来确认。或者,使用 var longs 而不是 BigInt 重写主要测试。

此外,ops.replicate 将操作派生到新线程中,而不是利用某种线程池。线程创建有一定的开销,但在这种情况下不足以成为问题。我个人更喜欢使用更强大的 java.util.concurrent 库。

【讨论】:

  • 感谢您的提示。我调查了一下,认为内存分配不是问题。即使我所做的不是“isProbablePrime”而是分配一个 52 MByte 的 Int-Array,其中写入一个常量,并行程序仍然比串行程序快 30%。远比蜜蜂慢 2.5 倍。
  • 你是在并行块内进行分配吗?
  • 如果我这样做,我会看到线性加速到我的机器上的核心数量: val a = isPrime(j) 其中: def isPrime(n:Int): Boolean = { for(i
  • 是的,我正在块内进行分配。我什至可以看到 MemoryUsage 上下跳跃,并行程序肯定比串行程序使用更多的内存。 (这是意料之中的)
  • 我的观点是,如果你想看到线性加速,你不应该在并行块中进行分配。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-08-25
  • 2019-12-09
  • 2015-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-27
相关资源
最近更新 更多