Scala: актер с состоянием, рекурсивный вызов быстрее, чем с помощью vars? - PullRequest
5 голосов
/ 29 апреля 2011

Пример кода ниже. Мне немного любопытно, почему MyActor быстрее, чем MyActor2. MyActor рекурсивно вызывает процесс / реагирует и сохраняет состояние в параметрах функции, тогда как 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% быстрее. Итак ... урок: если вы уверены, что у вас не останется переполнение стека невыполненными сообщениями, и вы хотите выжать каждую небольшую производительность ... не используйте loop и просто вызывайте 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()
    }

Ответы [ 3 ]

3 голосов
/ 29 апреля 2011

Такие результаты вызваны спецификой вашего теста (множество маленьких сообщений, которые заполняют почтовый ящик актера быстрее, чем он может их обработать).

Как правило, рабочий процесс react следующий:

  1. Актер сканирует почтовый ящик;
  2. Если он находит сообщение, он планирует выполнение ;
  3. Когда планирование завершено или когда в почтовом ящике нет сообщений, субъект приостанавливается (выбрасывается Actor.suspendException);

В первом случае, когда обработчик заканчивает обработку сообщения, выполнение переходит к методу react, и, пока в почтовом ящике много сообщений, субъект немедленно планирует следующее сообщение для выполнения, и только после этого приостанавливается.

Во втором случае loop планирует выполнение react, чтобы предотвратить переполнение стека (что может быть вашим случаем с Actor # 1, потому что tail рекурсия в process не оптимизирована), и, следовательно, выполнение не переходит к react сразу, как в первом случае. Вот где миллис теряются.


ОБНОВЛЕНИЕ (взято из здесь ):

Использование цикла вместо рекурсивного реагирования эффективно удваивает количество задачи, которые должен выполнять пул потоков выполнить для того, чтобы выполнить тот же объем работы, который в свою очередь делает так, что любые накладные расходы в планировщик гораздо более выражен, когда используя цикл.

0 голосов
/ 29 апреля 2011

Издержки в вашем тесте сильно зависят от количества присутствующих потоков (попробуйте использовать только один поток с scala -Dactors.corePoolSize=1!). Мне трудно понять, где именно возникает разница; единственная реальная разница в том, что в одном случае вы используете loop, а в другом - нет. Цикл выполняет большую часть работы, поскольку он многократно создает функциональные объекты, используя «andThen», а не итерацию Я не уверен, достаточно ли этого, чтобы объяснить разницу, особенно в свете интенсивного использования scala.actors.Scheduler$.impl и ExceptionBlob.

0 голосов
/ 29 апреля 2011

Просто дикий удар в темноте. Это может быть связано с исключением, вызванным реакцией для эвакуации цикла. Создание исключений довольно тяжелое. Однако я не знаю, как часто это происходит, но это можно проверить с помощью улова и счетчика.

...