【问题标题】:Scala performance with functional constructs具有功能结构的 Scala 性能
【发布时间】:2015-11-20 08:46:24
【问题描述】:

我目前正在分析一个用 Scala 编写的应用程序的性能,我想知道是否可以使用函数式构造。一方面我喜欢函数式编程的优雅和简洁,另一方面我害怕由此产生的性能。我发现了一个特别好的例子。

我有一个包含一百万个字符的字符串,我需要对每个数字求和。一个典型的函数式方法是这样的:

val sum = value.map(_.asDigit).sum.toString

但是,这种美观、简洁、实用的方法需要 0.98 秒(几乎是一秒)

var sum = 0;

for(digit <- value)
  sum += digit.asDigit

另一方面,这种命令式方法只需要 0.022 秒(上述时间的 2.24%) - 它快了大约 50 倍...

我确信问题的出现是因为 Scala 在第一种方法中生成了一个新列表,然后再次迭代该列表以创建总和。

依赖函数式构造只是一个坏主意吗?我的意思是,它们很漂亮——我爱它们——但它们的速度慢了 50 倍……

附: 我也尝试了其他方法。

val sum = value.foldLeft(0)((sum, value) => sum + value.asDigit)

这种函数式方法比命令式方法更不简洁,而且可能更难阅读,需要 0.085 秒。它更难阅读,而且速度仍然慢 4 倍......

【问题讨论】:

  • 您对map 的使用会创建一个您不需要的中间字符串。你应该可以通过使用value.view.map(_.asDigit).sum来避免它。

标签: scala


【解决方案1】:

首先:您确定您已经正确地对这两个版本进行了基准测试吗?仅使用 System.nanoTime 之类的方法测量执行时间不会给出准确的结果。请参阅 JVM 性能专家 Aleksey Shipilёv 撰写的这个有趣且富有洞察力的 blog post

这是一个使用出色的 Thyme scala 基准库的基准测试:

val value = "1234567890" * 100000
def sumf = value.map(_.asDigit).sum
def sumi = { var sum = 0; for(digit <- value) sum += digit.asDigit; sum }

val th = ichi.bench.Thyme.warmed(verbose = println)
scala> th.pbenchOffWarm("Functional vs. Imperative")(th.Warm(sumf))(th.Warm(sumi))
Benchmark comparison (in 6.654 s): Functional vs. Imperative
Significantly different (p ~= 0)
  Time ratio:    0.36877   95% CI 0.36625 - 0.37129   (n=20)
    First     40.25 ms   95% CI 40.15 ms - 40.34 ms
    Second    14.84 ms   95% CI 14.75 ms - 14.94 ms
res3: Int = 4500000

所以是的,命令式版本 更快。但几乎没有你测量的那么多。在许多情况下,性能差异将完全无关紧要。对于那些性能差异确实很重要的少数情况,scala 让您有机会编写命令式代码。总而言之,我认为 scala 做得很好。

顺便说一句:在正确进行基准测试时,您的第二种方法几乎与命令式版本一样快:

def sumf2 = value.foldLeft(0)(_ + _.asDigit)

scala> th.pbenchOffWarm("Functional2 vs. Imperative")(th.Warm(sumf2))(th.Warm(sumi))
Benchmark comparison (in 3.886 s): Functional2 vs. Imperative
Significantly different (p ~= 0)
  Time ratio:    0.89560   95% CI 0.88823 - 0.90297   (n=20)
    First     16.95 ms   95% CI 16.85 ms - 17.04 ms
    Second    15.18 ms   95% CI 15.08 ms - 15.27 ms
res17: Int = 4500000

根据@Odomontois 的建议进行更新:请注意,如果您真的想要优化此功能,则必须确保字符串的字符没有被装箱。这是一个命令式的版本,看起来不是很好,但也几乎是尽可能快的。这是使用 spire 中的 cfor 宏,但 while 循环也可以。

def sumi3 = {
  var sum = 0
  cfor(0)(_ < value.length, _ + 1) { i => 
    sum += value(i).asDigit
  }
  sum
}

scala> th.pbenchOffWarm("Imperative vs. optimized Imperative")(th.Warm(sumi))(th.Warm(sumi3))
Benchmark comparison (in 4.401 s): Imperative vs. optimized Imperative
Significantly different (p ~= 0)
  Time ratio:    0.08925   95% CI 0.08880 - 0.08970   (n=20)
    First     15.10 ms   95% CI 15.04 ms - 15.16 ms
    Second    1.348 ms   95% CI 1.344 ms - 1.351 ms
res9: Int = 4500000

过早的优化免责声明:

除非您绝对确定 a) 一段代码是性能瓶颈并且 b) 命令式版本要快得多,否则我总是更喜欢最易读的版本而不是最快的版本。 Scala 2.12 将附带一个new optimizer,这将使函数式样式的大量开销变得更小,因为它可以在许多情况下进行高级优化,例如闭包内联。

【讨论】:

  • 您的“命令式”版本不是非常必要的。它使用 scala 迭代器。有关性能替代品,请参阅spire's cfor 另请参阅this blog post
  • 这不是 my 命令式版本,而是来自 OP 的版本。这是一个真正尖叫,但丑陋的夜晚:def sumi2 = { var i = 0; var sum = 0; while(i &lt; value.length) { sum += value(i).asDigit; i += 1 }; sum }
  • 是的,对于误导的指责,我们深表歉意。但我提到的是“如果你在基准测试中也包含cfor 版本会很酷”。因为cfor 宏的扩展基本上是你刚刚在评论中写的,但不那么难看。
最近更新 更多