没有其他答案提到速度差异的主要原因,因为该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
这表明该zipped
版本的吞吐量提高了约80%,这可能与您的测量结果大致相同。
衡量分配
您也可以要求jmh使用以下方法衡量分配-prof gc
:
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
根据另一个答案中的评论在基准中添加了一个实现:
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
这就是我所期望的,因为调用函数本质上并不昂贵,并且通过索引访问数组元素非常便宜。