【问题标题】:How to efficiently combine future results as a future如何有效地结合未来的结果作为未来
【发布时间】:2019-09-23 08:27:39
【问题描述】:

我有很多计算对最终结果有贡献,对贡献的顺序没有限制。似乎 Futures 应该能够加快速度,而且他们确实做到了,但不像我想象的那样。下面是比较一种非常愚蠢的整数除法性能的代码:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

object scale_me_up {
  def main(args: Array[String]) {
    val M = 500 * 1000
    val N = 5
    Thread.sleep(3210) // let launcher settle down
    for (it <- 0 until 15) {
      val method = it % 3
      val start = System.currentTimeMillis()
      val result = divide(M, N, method)
      val elapsed = System.currentTimeMillis() - start
      assert(result == M / N)
      if (it >= 6) {
        val methods = Array("ordinary", "fast parallel", "nice parallel")
        val name = methods(method)
        println(f"$name%15s: $elapsed ms")
      }
    }
  }

  def is_multiple_of(m: Int, n: Int): Boolean = {
    val result = !(1 until n).map(_ + (m / n) * n).toSet.contains(m)
    assert(result == (m % n == 0)) // yes, a less crazy implementation exists
    result
  }

  def divide(m: Int, n: Int, method: Int): Int = {
    method match {
      case 0 =>
        (1 to m).count(is_multiple_of(_, n))
      case 1 =>
        (1 to m)
          .map { x =>
            Future { is_multiple_of(x, n) }
          }
          .count(Await.result(_, Duration.Inf))
      case 2 =>
        Await.result(divide_futuristically(m, n), Duration.Inf)
    }
  }

  def divide_futuristically(m: Int, n: Int): Future[Int] = {
    val futures = (1 to m).map { x =>
      Future { is_multiple_of(x, n) }
    }
    Future.foldLeft(futures)(0) { (count, flag) =>
      { if (flag) { count + 1 } else { count } }
    }
    /* much worse performing alternative:
    Future.sequence(futures).map(_.count(identity))
    */
  }
}

当我运行这个时,并行的case 1 比普通的case 0 代码要快一些(欢呼),但case 2 需要两倍的时间。当然,这取决于系统以及每个未来是否需要完成足够的工作(这里随着分母 N 增长)来抵消并发开销。 [PS] 正如预期的那样,在我的双核 CPU 上,减小 N 会使 case 0 领先,而将 N 增大到足以使 case 1case 2 的速度大约是 case 0 的两倍。

我被引导相信divide_futuristically 是表达这种计算的更好方式:返回带有组合结果的未来。阻塞只是我们在这里衡量性能所需要的东西。但实际上,越堵,大家越快完蛋。我究竟做错了什么?总结未来的几种替代方案(如sequence)都受到相同的惩罚。

[PPS] 这是在 Scala 2.12 上运行在 2 核 CPU 上的 Java 11 上。使用 6 核 CPU 上的 Java 12,差异就不那么明显了(尽管 sequence 的替代方案仍然拖了后腿)。使用 Scala 2.13,差异甚至更小,随着每次迭代工作量的增加,divide_futuristically 开始超越竞争对手。未来终于来了……

【问题讨论】:

  • 期货在这里帮不了你。它们不是线程,它们本质上是被安排在固定线程池上运行的函数。它们在托管非阻塞 IO 时最有用,而不是阻塞 CPU 操作。对于像您正在尝试做的数学繁重的事情,您需要 并行性, 就像 Scala 集合为您提供的那样 docs.scala-lang.org/overviews/parallel-collections/…
  • 也就是说,即使并行化您的算法也可能不会给您带来显着的提升,具体取决于具体情况,因为并行化具有幕后成本,例如协调多个线程。
  • 通过优化val result = !(1 until n).map(_ + (m / n) * n).toSet.contains(m),您可能会获得更好的性能
  • @ViktorKlang 您是否阅读了该声明下方的评论、断言、示例描述?我不是想发明整数除法,它只是可以并行化的愚蠢代码。
  • Future 与我惊人地平行:我使用通道和固定数量的线程在 Rust 和 Go 中实现了真正的算法(相同类型的顺序无关迭代,但有用且更复杂)。当平均迭代有很多工作要做时,Scala 实现更容易且更并行,但在迭代较少时会落后。 “更并行”是指并行 Scala 实现的速度是普通 Scala 实现的两倍,而 Rust 或 Go 中的并行度提升较少(但各方面都有很多需要调整的地方)。

标签: scala future


【解决方案1】:

看来你做的一切都是对的。我自己尝试了不同的方法,甚至.par,但得到了相同或更差的结果。

我已深入了解Future.foldLeft 并尝试分析导致延迟的原因:

  /** A non-blocking, asynchronous left fold over the specified futures,
   *  with the start value of the given zero.
   *  The fold is performed asynchronously in left-to-right order as the futures become completed.
   *  The result will be the first failure of any of the futures, or any failure in the actual fold,
   *  or the result of the fold.
   *
   *  Example:
   *  {{{
   *    val futureSum = Future.foldLeft(futures)(0)(_ + _)
   *  }}}
   *
   * @tparam T       the type of the value of the input Futures
   * @tparam R       the type of the value of the returned `Future`
   * @param futures  the `scala.collection.immutable.Iterable` of Futures to be folded
   * @param zero     the start value of the fold
   * @param op       the fold operation to be applied to the zero and futures
   * @return         the `Future` holding the result of the fold
   */
  def foldLeft[T, R](futures: scala.collection.immutable.Iterable[Future[T]])(zero: R)(op: (R, T) => R)(implicit executor: ExecutionContext): Future[R] =
    foldNext(futures.iterator, zero, op)

  private[this] def foldNext[T, R](i: Iterator[Future[T]], prevValue: R, op: (R, T) => R)(implicit executor: ExecutionContext): Future[R] =
    if (!i.hasNext) successful(prevValue)
    else i.next().flatMap { value => foldNext(i, op(prevValue, value), op) }

这部分:

else i.next().flatMap { value => foldNext(i, op(prevValue, value), op) }

.flatMap 生成一个新的 Future 并提交给 executor。换句话说,每一个

    { (count, flag) =>
      { if (flag) { count + 1 } else { count } }
    }

作为新的 Future 执行。

我想这部分会导致实验证明的延迟。

【讨论】:

  • 我仍然抱有太大希望,无法接受这个作为答案......也许它有助于链接期货,让每个人传递累积的结果?
  • @Stein 当然.flatMap 是链接期货的必要条件。由于ExcecutorContext 耗尽,在应用程序中通过Await 阻塞线程是不好的。它被设计为异步和非阻塞的。单个任务可能会更慢,但是当有数千个任务时,CPU 利用率会更高。
  • 嗯,这种链接是一种绝望的行为,但你很可能非常正确地认为阻塞在未来是一个特别糟糕的主意。性能很糟糕,慢了 20 倍以上(大部分是内核时间)。因此,阻止Future.sequence 也很糟糕。依靠andNext 在逻辑上更新共享结果需要阻塞(对于这个特定示例,原子整数就足够了)。我仍然不清楚flatMap 是如何进行同步的,但我想那就别无选择了。
  • @Stein Future.sequence 是非阻塞的。 /** Simple version of `Future.traverse`. Asynchronously and non-blockingly transforms a `TraversableOnce[Future[A]]` * into a `Future[TraversableOnce[A]]`. Useful for reducing many `Future`s into a single `Future`. * * @tparam A the type of the value inside the Futures * @tparam M the type of the `TraversableOnce` of Futures * @param in the `TraversableOnce` of Futures which will be sequenced * @return the `Future` of the `TraversableOnce` of results */ def sequence...
  • @Stein,所以Future.sequence 还不错。这是一个值得做的方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-02
  • 2016-04-02
相关资源
最近更新 更多