【问题标题】:Akka - worse performance with more actorsAkka - 更多演员的表现更差
【发布时间】:2017-01-21 20:13:33
【问题描述】:

我正在尝试使用 Scala 和 Akka 进行一些并行编程,我是新手。我有一个非常简单的 Monte Carlo Pi 应用程序(在一个圆圈中近似 pi),我用多种语言构建了它。然而,我在 Akka 中构建的版本的性能让我感到困惑。

我有一个用纯 Scala 编写的顺序版本,通常需要大约 400 毫秒才能完成。

与 1 个 worker actor 相比,Akka 版本大约需要 300-350 毫秒,但是随着我增加 actor 的数量,时间会急剧增加。对于 4 个演员,时间可以在 500 毫秒到 1200 毫秒或更高之间的任何地方。

迭代次数在工作角色之间分配,因此理想情况下,他们越多,性能应该越好,目前它变得越来越差。

我的代码是

object MCpi{
  //Declare initial values
  val numWorkers = 2
  val numIterations = 10000000

  //Declare messages that will be sent to actors
  sealed trait PiMessage
  case object Calculate extends PiMessage
  case class Work(iterations: Int) extends PiMessage
  case class Result(value: Int) extends PiMessage
  case class PiApprox(pi: Double, duration: Double)

  //Main method
  def main(args: Array[String]): Unit = {
    val system = ActorSystem("MCpi_System") //Create Akka system
    val master = system.actorOf(Props(new MCpi_Master(numWorkers, numIterations))) //Create Master Actor
    println("Starting Master")

    master ! Calculate //Run calculation
  }
}

//Master
class MCpi_Master(numWorkers: Int, numIterations: Int) extends Actor{

  var pi: Double = _ // Store pi
  var quadSum: Int = _ //the total number of points inside the quadrant
  var numResults: Int = _ //number of results returned
  val startTime: Double = System.currentTimeMillis() //calculation start time

  //Create a group of worker actors
  val workerRouter = context.actorOf(
    Props[MCpi_Worker].withRouter(RoundRobinPool(numWorkers)), name = "workerRouter")
  val listener = context.actorOf(Props[MCpi_Listener], name = "listener")

  def receive = {
    //Tell workers to start the calculation
      //For each worker a message is sent with the number of iterations it is to perform,
      //iterations are split up between the number of workers.
    case Calculate => for(i <- 0 until numWorkers) workerRouter ! Work(numIterations / numWorkers);

      //Receive the results from the workers
        case Result(value) =>
            //Add up the total number of points in the circle from each worker
      quadSum += value
            //Total up the number of results which have been received, this should be 1 for each worker
      numResults += 1

      if(numResults == numWorkers) { //Once all results have been collected
          //Calculate pi
          pi = (4.0 * quadSum) / numIterations
          //Send the results to the listener to output
        listener ! PiApprox(pi, duration = System.currentTimeMillis - startTime)
        context.stop(self)
      }
  }
}
//Worker
class MCpi_Worker extends Actor {
  //Performs the calculation
  def calculatePi(iterations: Int): Int = {

    val r = scala.util.Random // Create random number generator
    var inQuadrant: Int = 0 //Store number of points within circle

    for(i <- 0 to iterations){
      //Generate random point
      val X = r.nextFloat()
      val Y = r.nextFloat()

      //Determine whether or not the point is within the circle
      if(((X * X) + (Y * Y)) < 1.0)
        inQuadrant += 1
    }
    inQuadrant //return the number of points within the circle
  }

  def receive = {
    //Starts the calculation then returns the result
    case Work(iterations) => sender ! Result(calculatePi(iterations))
  }
}

//Listener
class MCpi_Listener extends Actor{ //Recieves and prints the final result
  def receive = {
    case PiApprox(pi, duration) =>
        //Print the results
      println("\n\tPi approximation: \t\t%s\n\tCalculation time: \t%s".format(pi, duration))
        //Print to a CSV file
        val pw: FileWriter = new FileWriter("../../../..//Results/Scala_Results.csv", true)
        pw.append(duration.toString())
        pw.append("\n")
        pw.close()
      context.system.terminate()

  }
}

普通的 Scala 顺序版本是

object MCpi {
    def main(args: Array[String]): Unit = {
        //Define the number of iterations to perform
        val iterations = args(0).toInt;
        val resultsPath = args(1);

        //Get the current time
        val start = System.currentTimeMillis()


        // Create random number generator
        val r = scala.util.Random
        //Store number of points within circle
        var inQuadrant: Int = 0

        for(i <- 0 to iterations){
            //Generate random point
            val X = r.nextFloat()
            val Y = r.nextFloat()

            //Determine whether or not the point is within the circle
            if(((X * X) + (Y * Y)) < 1.0)
                inQuadrant += 1
        }
        //Calculate pi
        val pi = (4.0 * inQuadrant) / iterations
        //Get the total time
        val time = System.currentTimeMillis() - start
        //Output values
        println("Number of Iterations: " + iterations)
        println("Pi has been calculated as: " + pi)
        println("Total time taken: " + time + " (Milliseconds)")

        //Print to a CSV file
        val pw: FileWriter = new FileWriter(resultsPath + "/Scala_Results.csv", true)
        pw.append(time.toString())
        pw.append("\n")
        pw.close()
    }
}

非常欢迎任何关于为什么会发生这种情况或如何提高性能的建议。

编辑:我要感谢大家的回答,这是我在这个网站上的第一个问题,所有的答案都非常有帮助,我现在有很多东西要看:)

【问题讨论】:

  • 在这种情况下,有关您运行它的处理器类型的一些信息可能会有所帮助。
  • 1) 请在 SO 上发布您的代码。在发布之前对其进行格式化。 2) 当您多次执行calculatePi 方法时,您甚至对actor 实现有什么期望,这是我所看到的,相当于您的顺序实现?从我所见,您只是多次计算 PI(计算次数等于工人演员的数量,这可能是减速的原因)?如我错了请纠正我。 3) 您是否考虑过在这种情况下使用 Actor 模型可能不会获得任何收益?
  • @Jasper-M 处理器是 Intel i7-4510U 四核 @ 3.1GHz @Branislav 1) 好的,我稍后有空时会尝试用代码更新帖子。 2) calculatePi 由每个工人运行,它生成许多随机点并测量这些点是否在特定大小的“圆圈”内(在本例中为 1.0),然后返回圆圈中有多少点(quadSum) ,一旦从每个工人那里得到结果,计算就会完成一次,以计算出 Pi 是什么(在主演员中)。 3) 我假设我会得到某种性能提升,将工作分配给多个演员。
  • 这是一个 CPU 密集型任务,参与者共享一个线程池,因此添加更多参与者而不配置池以托管更多线程会降低性能。请记住,actor 模式是一种用于并发通信的工具,而不是用于并行计算
  • @MustafaSimav 在四核上仍然有 2 个演员而不是 1 个演员可能会显示出加速。 Actor 可用于并行计算以及并发通信。

标签: performance scala parallel-processing akka


【解决方案1】:

您正在使用的Random 实例存在同步问题。

更具体地说,这一行

val r = scala.util.Random // Create random number generator

实际上并没有“创建随机数生成器”,而是选择了 scala.util 方便地为您提供的单例 object。这意味着所有线程都将共享它,并将围绕其种子进行同步(有关更多信息,请参阅java.util.Random.nextFloat 的代码)。

只需将该行更改为

val r = new scala.util.Random // Create random number generator

您应该获得一些并行化加速。如 cmets 中所述,加速将取决于您的架构等,但至少不会因强同步而严重偏向。

注意java.util 将使用System.nanoTime 作为新创建的Random 的种子,因此您不必担心随机化问题。

【讨论】:

  • 谢谢,我不知道这一点,知道这真的很有帮助。
  • 也不知道,我发现调查你的例子:) 很好的学习!你能看到加速吗?
  • 刚刚有机会实施您的建议,哇,大大加快了速度。结果现在更符合我的预期。非常感谢。
【解决方案2】:

我认为这是一个值得深入研究的好问题。使用确实会带来一些系统开销的 Akka Actor 系统,我预计只有在规模足够大时才能看到性能提升。我以最少的代码更改测试了您的两个版本(非 akka 与 akka)。正如预期的那样,在 100 万或 1000 万次点击时,无论 Akka 与非 Akka 或使用的工人数量如何,几乎没有任何性能差异。但在 1 亿次点击时,您可以看到一致的性能差异。

除了将总点击量扩大到 1 亿之外,我所做的唯一代码更改是将 scala.util.Random 替换为 java.util.concurrent.ThreadLocalRandom:

//val r = scala.util.Random // Create random number generator
def r = ThreadLocalRandom.current
...
  //Generate random point
  //val X = r.nextFloat()
  //val Y = r.nextFloat()
  val X = r.nextDouble(0.0, 1.0)
  val Y = r.nextDouble(0.0, 1.0)

这一切都是在配备 2GHz 四核 CPU 和 8GB 内存的旧 MacBook Pro 上完成的。以下是总点击量为 1 亿次的测试运行结果:

  • 非 Akka 应用大约需要 1720 毫秒
  • 有 2 个工作人员的 Akka 应用程序需要大约 770 毫秒
  • 具有 4 个工作人员的 Akka 应用程序需要大约 430 毫秒

下面的单独测试运行...

非 Akka

$ sbt "runMain calcpi.MCpi 100000000 /tmp"

[info] 从 /Users/leo/projects/scala/test/akka-calculate-pi/project 加载项目定义 [信息] 将当前项目设置为 Akka Pi Calculation(在构建文件中:/Users/leo/projects/scala/test/akka-calculate-pi/) [信息] 运行 calcpi.MCpi 100000000 /tmp 迭代次数:100000000 Pi 已计算为:3.1415916 总时间:1722(毫秒) [成功] 总时间:2s,2017年1月20日下午3:26:20完成

$ sbt "runMain calcpi.MCpi 100000000 /tmp"

[info] 从 /Users/leo/projects/scala/test/akka-calculate-pi/project 加载项目定义 [信息] 将当前项目设置为 Akka Pi Calculation(在构建文件中:/Users/leo/projects/scala/test/akka-calculate-pi/) [信息] 运行 calcpi.MCpi 100000000 /tmp 迭代次数:100000000 Pi 已计算为:3.14159724 总时间:1715(毫秒) [成功] 总时间:2s,2017年1月20日下午3:28:17完成

使用 Akka

工人数 = 4:

$ sbt "runMain calcpi.MCpi 100000000 /tmp"

[info] 从 /Users/leo/projects/scala/test/akka-calculate-pi/project 加载项目定义 [信息] 将当前项目设置为 Akka Pi Calculation(在构建文件中:/Users/leo/projects/scala/test/akka-calculate-pi/) [信息] 运行 calcpi.MCpi 100000000 /tmp 起始大师

Pi approximation:       3.14110116
Calculation time:   423.0

[成功]总时间:1秒,2017年1月20日下午3:35:25完成

$ sbt "runMain calcpi.MCpi 100000000 /tmp"

[info] 从 /Users/leo/projects/scala/test/akka-calculate-pi/project 加载项目定义 [信息] 将当前项目设置为 Akka Pi Calculation(在构建文件中:/Users/leo/projects/scala/test/akka-calculate-pi/) [信息] 运行 calcpi.MCpi 100000000 /tmp 起始大师

Pi approximation:       3.14181316
Calculation time:   440.0

[成功]总时间:1秒,2017年1月20日下午3:35:34完成

工人数 = 2:

$ sbt "runMain calcpi.MCpi 100000000 /tmp"

[info] 从 /Users/leo/projects/scala/test/akka-calculate-pi/project 加载项目定义 [信息] 将当前项目设置为 Akka Pi Calculation(在构建文件中:/Users/leo/projects/scala/test/akka-calculate-pi/) [信息] 运行 calcpi.MCpi 100000000 /tmp 起始大师

Pi approximation:       3.14162344
Calculation time:   766.0

[成功]总时间:2秒,2017年1月20日下午3:36:34完成

$ sbt "runMain calcpi.MCpi 100000000 /tmp"

[info] 从 /Users/leo/projects/scala/test/akka-calculate-pi/project 加载项目定义 [信息] 将当前项目设置为 Akka Pi Calculation(在构建文件中:/Users/leo/projects/scala/test/akka-calculate-pi/) [信息] 运行 calcpi.MCpi 100000000 /tmp 起始大师

Pi approximation:       3.14182148
Calculation time:   787.0

[成功] 总时间:2s,2017年1月20日下午3:36:43完成

【讨论】:

  • 你完全正确,问题是我使用的随机数生成器。规模越大,速度差异也越大。非常感谢你:)
【解决方案3】:

我认为您的问题是由于在接收函数的主体中执行繁重的计算引起的,其中一些可能在一个线程上运行,因此您只是将 aktor 系统权重添加到您的标准单线程计算中,从而使其变慢。来自 akka 文档:

在幕后 Akka 将在一组真实线程上运行一组 Actor,通常许多 Actor 共享一个线程,并且随后对一个 Actor 的调用可能最终在不同线程上处理。 Akka 确保此实现细节不会影响处理 actor 状态的单线程。

我不确定是否是这种情况,但您可以在以后尝试运行您的计算:

Future {
  //your code
}

要使其工作,您需要提供隐式执行上下文,您可以通过多种方式做到这一点,但最简单的有两种:

  1. 导入全局执行上下文

  2. 导入actor的执行上下文:

    导入 context.dispatcher

第二个必须用于你的actor类主体。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-04-23
    • 2014-03-09
    • 2019-01-16
    • 2015-03-25
    • 2012-10-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多