Как уменьшить переключение контекста при обходе Seq из Scala фьючерсов - PullRequest
1 голос
/ 18 февраля 2020

У меня есть программа, которая должна извлечь большое количество (~ 300) записей и выполнить с ними некоторые операции. Записи кэшируются, так что это не занимает процессорного времени и почти не требует времени. Однако при использовании ЦП 50% задержка p95 составляет 40 мс. При просмотре трассировки стека потоки проводят большую часть времени в состоянии «припарковано», и наиболее часто используемым методом является «java .util.concurrent.locks.LockSupport.parkNanos (LockSupport. java: 215)». Поэтому я предполагаю, что большую часть времени используется для переключения контекста

 def getObject(id: Ing): Future[SomeObject] = ??? // Can make a call to DB, but usually cached
 val ids: Seq[Int] = ??? // 300 ints

 Future.iterate(ids)(getObject)

Как я могу улучшить производительность? Я пробовал Akka FastFuture и пытался использовать как глобальный (пул потоков рабочей кражи), так и диспетчер Akka по умолчанию (ForkJoin).

Конкретно 1) Что я могу сделать, если предположить, что некоторые из getObject вызывают блокировку (кеш) пропустите) 2) Что я могу сделать, если предположить, что все вызовы getObject обслуживаются из кэша (я могу предварительно активировать кэш и использовать фоновый refre sh)

ps Дамп кучи: https://heaphero.io/my-heap-report.jsp?p=YXJjaGl2ZWQvMjAyMC8wMi8xOC8tLWJhc2UtZm9yay56aXAtNC0yNi02Lmpzb24tLQ==

Ответы [ 2 ]

2 голосов
/ 18 февраля 2020

Аккуратный трюк, позволяющий избежать переключения контекста при использовании Scala Future s, заключается в использовании parasitic в качестве ExecutionContext, который "крадет время выполнения из других потоков, поскольку его Runnables запускаются в потоке, который вызывает execute и затем возвращение контроля вызывающей стороне после того, как все были выполнены его Runnables ". parasitic доступен с Scala 2.13, но вы можете легко понять это и перенести его на проекты до 2.13, посмотрев его код (здесь для версии 2.13.1) . Наивная, но работающая реализация для проектов до 2.13 просто запускает Runnable s, не заботясь о распределении их в потоке, что делает трюк, как в следующем фрагменте:

object parasitic212 extends ExecutionContext {

  override def execute(runnable: Runnable): Unit =
    runnable.run()

  // reporting failures is left as an exercise for the reader
  override def reportFailure(cause: Throwable): Unit = ???

}

The parasitic реализация, конечно, более нюансированная. Для более глубокого понимания рассуждений и некоторых предостережений относительно их использования я бы предложил вам сослаться на PR и представленный parasitic как на общедоступный API (он уже был реализован, но зарезервирован для внутреннего использования).

Цитируя оригинальное описание PR:

В рамках реализации Future долгое время использовался синхронный трамплин, ExecutionContext для запуска управляемого лога c как можно дешевле.

Я полагаю, что существует значительное количество случаев, когда для эффективности имеет смысл выполнять логически c синхронно безопасным (-i sh) способом, не имея пользователей для реализации логики c для этого ExecutionContext - это сложно реализовать, если не сказать больше.

Важно помнить, что ExecutionContext должен быть предоставлен через неявный параметр, чтобы вызывающая сторона могла решить, где должен logi c быть казненным. Использование ExecutionContext.parasiti c означает, что logi c может в конечном итоге работать в потоках / пулах, которые не были предназначены или предназначены для запуска указанных logi c. Например, вы можете в конечном итоге запустить привязанную к ЦП логи c в пуле, спроектированном на IO, или наоборот. Поэтому использование parasiti c рекомендуется только тогда, когда это действительно имеет смысл. Существует также реальный риск применения StackOverflowErrors для определенных шаблонов вложенных вызовов, когда глубокая цепочка вызовов заканчивается в исполнителе parasiti c, что приводит к еще большему использованию стека в последующем выполнении. В настоящее время parasiti c ExecutionContext разрешает вложенную последовательность вызовов максимум 16, это может быть изменено в будущем, если будет обнаружено, что это вызывает проблемы.

Как указано в официальном Документация для parasitic, рекомендуется использовать только тогда, когда исполняемый код быстро возвращает управление вызывающей стороне. Вот документация, приведенная для версии 2.13.1:

ПРЕДУПРЕЖДЕНИЕ: всегда выполняйте только logi c, который быстро вернет управление вызывающей стороне.

Этот ExecutionContext крадет время выполнения у другие потоки, запустив свои Runnables в потоке, который вызывает execute и затем возвращает вызывающему элементу контроль после того, как all его Runnables были выполнены. Вложенные вызовы execute будут запутаны для предотвращения неконтролируемого увеличения пространства стека.

При использовании parasiti c с такими абстракциями, как Future, во многих случаях будет не определено c относительно того, какой поток будет выполняться лог c, поскольку это зависит от того, когда / если это Будущее завершено.

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

Симптомы неправильного использования этого ExecutionContext включают, но не ограничиваются этим, взаимоблокировки и серьезные проблемы с производительностью.

Любые исключения NonFatal или InterruptedException будут сообщаться defaultReporter.

2 голосов
/ 18 февраля 2020

Лучшее решение состоит в том, чтобы изменить доступ к базе данных, чтобы несколько объектов выбирались в пределах одного Future. Даже если количество объектов, которые можно извлечь, ограничено, это все равно существенно сократит накладные расходы.

...