Scala 有状态演员,递归调用比使用变量更快?

发布于 2024-11-04 04:05:28 字数 2813 浏览 0 评论 0原文

下面的示例代码。我有点好奇为什么 MyActor 比 MyActor2 快。 MyActor 递归调用 process/react 并将状态保存在函数参数中,而 MyActor2 将状态保存在变量中。 MyActor 甚至有额外的状态元组开销,但仍然运行得更快。我想知道对此是否有一个很好的解释,或者我是否做了一些“错误”的事情。

我意识到性能差异并不显着,但事实是它存在并且一致,这让我很好奇这里发生了什么。

忽略前两次作为热身的运行,我得到:

MyActor: 第559章 511 第544章 529

MyActor2: 第647章 613 第654章 610

import scala.actors._

object Const {
  val NUM = 100000
  val NM1 = NUM - 1
}

trait Send[MessageType] {
  def send(msg: MessageType)
}

// Test 1 using recursive calls to maintain state

abstract class StatefulTypedActor[MessageType, StateType](val initialState: StateType) extends Actor with Send[MessageType] {
  def process(state: StateType, message: MessageType): StateType

  def act = proc(initialState)

  def send(message: MessageType) = {
    this ! message
  }

  private def proc(state: StateType) {
    react {
      case msg: MessageType => proc(process(state, msg))
    }
  }
}

object MyActor extends StatefulTypedActor[Int, (Int, Long)]((0, 0)) {
  override def process(state: (Int, Long), input: Int) = input match {
    case 0 =>
      (1, System.currentTimeMillis())
    case input: Int =>
      state match {
        case (Const.NM1, start) =>
          println((System.currentTimeMillis() - start))
          (Const.NUM, start)
        case (s, start) =>
          (s + 1, start)
      }
  }
}

// Test 2 using vars to maintain state

object MyActor2 extends Actor with Send[Int] {
  private var state = 0
  private var strt = 0: Long

  def send(message: Int) = {
    this ! message
  }

  def act =
    loop {
      react {
        case 0 =>
          state = 1
          strt = System.currentTimeMillis()
        case input: Int =>
          state match {
            case Const.NM1 =>
              println((System.currentTimeMillis() - strt))
              state += 1
            case s =>
              state += 1
          }
      }
    }
}


// main: Run testing

object TestActors {
  def main(args: Array[String]): Unit = {
    val a = MyActor
    //    val a = MyActor2
    a.start()
    testIt(a)
  }

  def testIt(a: Send[Int]) {
    for (_ <- 0 to 5) {
      for (i <- 0 to Const.NUM) {
        a send i
      }
    }
  }
}

编辑:根据瓦西尔的回应,我删除了循环并再次尝试。然后基于变量的 MyActor2 实现了跨越式发展,现在可能快了 10% 左右。所以......教训是:如果你确信你不会最终导致堆栈溢出积压的消息,并且你关心挤出每一点性能......不要使用循环,只需调用 act()方法递归。

MyActor2 的更改:

  override def act() =
    react {
      case 0 =>
        state = 1
        strt = System.currentTimeMillis()
        act()
      case input: Int =>
        state match {
          case Const.NM1 =>
            println((System.currentTimeMillis() - strt))
            state += 1
          case s =>
            state += 1
        }
        act()
    }

Sample code below. I'm a little curious why MyActor is faster than MyActor2. MyActor recursively calls process/react and keeps state in the function parameters whereas MyActor2 keeps state in vars. MyActor even has the extra overhead of tupling the state but still runs faster. I'm wondering if there is a good explanation for this or if maybe I'm doing something "wrong".

I realize the performance difference is not significant but the fact that it is there and consistent makes me curious what's going on here.

Ignoring the first two runs as warmup, I get:

MyActor:
559
511
544
529

vs.

MyActor2:
647
613
654
610

import scala.actors._

object Const {
  val NUM = 100000
  val NM1 = NUM - 1
}

trait Send[MessageType] {
  def send(msg: MessageType)
}

// Test 1 using recursive calls to maintain state

abstract class StatefulTypedActor[MessageType, StateType](val initialState: StateType) extends Actor with Send[MessageType] {
  def process(state: StateType, message: MessageType): StateType

  def act = proc(initialState)

  def send(message: MessageType) = {
    this ! message
  }

  private def proc(state: StateType) {
    react {
      case msg: MessageType => proc(process(state, msg))
    }
  }
}

object MyActor extends StatefulTypedActor[Int, (Int, Long)]((0, 0)) {
  override def process(state: (Int, Long), input: Int) = input match {
    case 0 =>
      (1, System.currentTimeMillis())
    case input: Int =>
      state match {
        case (Const.NM1, start) =>
          println((System.currentTimeMillis() - start))
          (Const.NUM, start)
        case (s, start) =>
          (s + 1, start)
      }
  }
}

// Test 2 using vars to maintain state

object MyActor2 extends Actor with Send[Int] {
  private var state = 0
  private var strt = 0: Long

  def send(message: Int) = {
    this ! message
  }

  def act =
    loop {
      react {
        case 0 =>
          state = 1
          strt = System.currentTimeMillis()
        case input: Int =>
          state match {
            case Const.NM1 =>
              println((System.currentTimeMillis() - strt))
              state += 1
            case s =>
              state += 1
          }
      }
    }
}


// main: Run testing

object TestActors {
  def main(args: Array[String]): Unit = {
    val a = MyActor
    //    val a = MyActor2
    a.start()
    testIt(a)
  }

  def testIt(a: Send[Int]) {
    for (_ <- 0 to 5) {
      for (i <- 0 to Const.NUM) {
        a send i
      }
    }
  }
}

EDIT: Based on Vasil's response, I removed the loop and tried it again. And then MyActor2 based on vars leapfrogged and now might be around 10% or so faster. So... lesson is: if you are confident that you won't end up with a stack overflowing backlog of messages, and you care to squeeze every little performance out... don't use loop and just call the act() method recursively.

Change for MyActor2:

  override def act() =
    react {
      case 0 =>
        state = 1
        strt = System.currentTimeMillis()
        act()
      case input: Int =>
        state match {
          case Const.NM1 =>
            println((System.currentTimeMillis() - strt))
            state += 1
          case s =>
            state += 1
        }
        act()
    }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

国际总奸 2024-11-11 04:05:29

这样的结果是由基准的具体情况引起的(许多小消息填满参与者的邮箱的速度比其处理它们的速度快)。

一般来说, react的工作流程如下:

  1. Actor扫描邮箱;
  2. 如果它找到一条消息,它安排执行
  3. 当调度完成时,或者当邮箱中没有消息时,actor 挂起(抛出Actor.suspendException);

在第一种情况下,当处理程序完成处理消息时,执行直接进行到 react 方法,并且只要邮箱中有大量消息,actor 就会立即调度下一条消息执行,并且仅在挂起之后。

在第二种情况下, 循环 安排 react 的执行,以防止堆栈溢出(这可能是 Actor #1 的情况,因为尾递归在 process 中未进行优化),因此,执行不会立即进行到 react,如第一种情况。这就是米利斯丢失的地方。


更新(取自在这里):

使用循环代替递归反应
有效地使数量增加了一倍
线程池必须执行的任务
执行以完成
相同的工作量,反过来
使得任何开销
调度程序在以下情况下更加明显
使用循环。

Such results are caused with the specifics of your benchmark (a lot of small messages that fill the actor's mailbox quicker than it can handle them).

Generally, the workflow of react is following:

  1. Actor scans the mailbox;
  2. If it finds a message, it schedules the execution;
  3. When the scheduling completes, or, when there're no messages in the mailbox, actor suspends (Actor.suspendException is thrown);

In the first case, when the handler finishes to process the message, execution proceeds straight to react method, and, as long as there're lots of messages in the mailbox, actor immediately schedules the next message to execute, and only after that suspends.

In the second case, loop schedules the execution of react in order to prevent a stack overflow (which might be your case with Actor #1, because tail recursion in process is not optimized), and thus, execution doesn't proceed to react immediately, as in the first case. That's where the millis are lost.


UPDATE (taken from here):

Using loop instead of recursive react
effectively doubles the number of
tasks that the thread pool has to
execute in order to accomplish the
same amount of work, which in turn
makes it so any overhead in the
scheduler is far more pronounced when
using loop.

暮光沉寂 2024-11-11 04:05:29

只是在黑暗中疯狂的一击。这可能是由于react为了退出循环而引发的异常。异常的创建相当繁重。然而我不知道它多久这样做一次,但这应该可以通过捕获和计数器进行检查。

Just a wild stab in the dark. It might be due to the exception thrown by react in order to evacuate the loop. Exception creation is quite heavy. However I don't know how often it do that, but that should be possible to check with a catch and a counter.

意中人 2024-11-11 04:05:29

测试的开销在很大程度上取决于存在的线程数量(尝试仅使用一个带有 scala -Dactors.corePoolSize=1 的线程!)。我发现很难准确地弄清楚差异出现在哪里;唯一真正的区别是,在一种情况下您使用循环,而在另一种情况下则不使用。循环确实做了相当多的工作,因为它使用“andThen”而不是迭代来重复创建函数对象。我不确定这是否足以解释差异,特别是考虑到 scala.actors.Scheduler$.impl 和 ExceptionBlob 的大量使用。

The overhead on your test depends heavily on the number of threads that are present (try using only one thread with scala -Dactors.corePoolSize=1!). I'm finding it difficult to figure out exactly where the difference arises; the only real difference is that in one case you use loop and in the other you do not. Loop does do fair bit of work, since it repeatedly creates function objects using "andThen" rather than iterating. I'm not sure whether this is enough to explain the difference, especially in light of the heavy usage by scala.actors.Scheduler$.impl and ExceptionBlob.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文