【发布时间】: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 1 和 case 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 中的并行度提升较少(但各方面都有很多需要调整的地方)。