没有其他答案提到速度差异的主要原因,即
zipped
版本避免了10,000个元组分配。正如其他答案中所指出的,
zip
版本涉及一个中间数组,而
zipped
版本则不涉及,但是为10,000个元素分配一个数组并不是使
zip
版本变得更糟糕的原因——而是被放入该数组中的10,000个短暂元组。这些在JVM上表示为对象,因此您正在为您立即要丢弃的东西进行一堆对象分配。
本答案的其余部分只是详细介绍了如何确认这一点。
更好的基准测试
您确实希望使用像jmh这样的框架,在JVM上负责任地进行任何基准测试,即使是设置jmh本身也不太难,但是负责任 部分很难。如果您有像这样的project/plugins.sbt
:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
以下是一个类似于这样的 build.sbt
文件(我使用2.11.8,因为您提到您正在使用它):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
然后,您可以像这样编写基准测试代码:
package zipped_bench
import org.openjdk.jmh.annotations._
@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class ZippedBench {
val arr1 = Array.fill(10000)(math.random)
val arr2 = Array.fill(10000)(math.random)
def ES(arr: Array[Double], arr1: Array[Double]): Array[Double] =
arr.zip(arr1).map(x => x._1 + x._2)
def ES1(arr: Array[Double], arr1: Array[Double]): Array[Double] =
(arr, arr1).zipped.map((x, y) => x + y)
@Benchmark def withZip: Array[Double] = ES(arr1, arr2)
@Benchmark def withZipped: Array[Double] = ES1(arr1, arr2)
}
使用sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 zipped_bench.ZippedBench"
来运行它:
Benchmark Mode Cnt Score Error Units
ZippedBench.withZip thrpt 20 4902.519 ± 41.733 ops/s
ZippedBench.withZipped thrpt 20 8736.251 ± 36.730 ops/s
这表明使用压缩版本可以获得大约80%的吞吐量,这可能与你的测量结果差不多。
测量分配也可以通过
-prof gc
命令来请求jmh进行。
Benchmark Mode Cnt Score Error Units
ZippedBench.withZip thrpt 5 4894.197 ± 119.519 ops/s
ZippedBench.withZip:·gc.alloc.rate thrpt 5 4801.158 ± 117.157 MB/sec
ZippedBench.withZip:·gc.alloc.rate.norm thrpt 5 1080120.009 ± 0.001 B/op
ZippedBench.withZip:·gc.churn.PS_Eden_Space thrpt 5 4808.028 ± 87.804 MB/sec
ZippedBench.withZip:·gc.churn.PS_Eden_Space.norm thrpt 5 1081677.156 ± 12639.416 B/op
ZippedBench.withZip:·gc.churn.PS_Survivor_Space thrpt 5 2.129 ± 0.794 MB/sec
ZippedBench.withZip:·gc.churn.PS_Survivor_Space.norm thrpt 5 479.009 ± 179.575 B/op
ZippedBench.withZip:·gc.count thrpt 5 714.000 counts
ZippedBench.withZip:·gc.time thrpt 5 476.000 ms
ZippedBench.withZipped thrpt 5 11248.964 ± 43.728 ops/s
ZippedBench.withZipped:·gc.alloc.rate thrpt 5 3270.856 ± 12.729 MB/sec
ZippedBench.withZipped:·gc.alloc.rate.norm thrpt 5 320152.004 ± 0.001 B/op
ZippedBench.withZipped:·gc.churn.PS_Eden_Space thrpt 5 3277.158 ± 32.327 MB/sec
ZippedBench.withZipped:·gc.churn.PS_Eden_Space.norm thrpt 5 320769.044 ± 3216.092 B/op
ZippedBench.withZipped:·gc.churn.PS_Survivor_Space thrpt 5 0.360 ± 0.166 MB/sec
ZippedBench.withZipped:·gc.churn.PS_Survivor_Space.norm thrpt 5 35.245 ± 16.365 B/op
ZippedBench.withZipped:·gc.count thrpt 5 863.000 counts
ZippedBench.withZipped:·gc.time thrpt 5 447.000 ms
…其中gc.alloc.rate.norm
可能是最有趣的部分,显示zip
版本分配的内存比zipped
多了三倍。
命令式实现
如果我知道这个方法将在极度注重性能的情况下被调用,我可能会像这样实现它:
def ES3(arr: Array[Double], arr1: Array[Double]): Array[Double] = {
val minSize = math.min(arr.length, arr1.length)
val newArr = new Array[Double](minSize)
var i = 0
while (i < minSize) {
newArr(i) = arr(i) + arr1(i)
i += 1
}
newArr
}
请注意,与其他答案中的优化版本不同,这个实现使用了
while
而不是
for
,因为
for
仍将被转换为Scala集合操作。我们可以比较这个实现(
withWhile
),其他答案的优化(但不是原地)实现(
withFor
)和两个原始实现:
Benchmark Mode Cnt Score Error Units
ZippedBench.withFor thrpt 20 118426.044 ± 2173.310 ops/s
ZippedBench.withWhile thrpt 20 119834.409 ± 527.589 ops/s
ZippedBench.withZip thrpt 20 4886.624 ± 75.567 ops/s
ZippedBench.withZipped thrpt 20 9961.668 ± 1104.937 ops/s
这两种版本之间的差别非常大,所有这些方法签名都完全相同,实现具有相同的语义。不像命令式实现使用全局状态等。尽管
zip
和
zipped
版本更易读,但我个人认为,命令式版本并没有违背"Scala精神"的任何意义,我会毫不犹豫地使用它们。
使用tabulate:
更新:根据另一个答案中的评论,我添加了一个基于
tabulate
的实现到基准测试中。
def ES4(arr: Array[Double], arr1: Array[Double]): Array[Double] = {
val minSize = math.min(arr.length, arr1.length)
Array.tabulate(minSize)(i => arr(i) + arr1(i))
}
它比zip
版本快得多,但仍然比命令式版本慢得多:
Benchmark Mode Cnt Score Error Units
ZippedBench.withTabulate thrpt 20 32326.051 ± 535.677 ops/s
ZippedBench.withZip thrpt 20 4902.027 ± 47.931 ops/s
这正是我所期望的,因为调用函数本身并不会很耗费资源,而通过索引访问数组元素也非常便宜。