为什么在 Scala 中压缩比 zip 快?
Why is zipped faster than zip in Scala?
我编写了一些 Scala 代码来对集合执行元素级操作。这里我定义了两个执行相同任务的方法。一种方法使用 zip
,另一种方法使用 zipped
.
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)
为了比较这两种方法的速度,我写了下面的代码:
def fun (arr : Array[Double] , arr1 : Array[Double] , f :(Array[Double],Array[Double]) => Array[Double] , itr : Int) ={
val t0 = System.nanoTime()
for (i <- 1 to itr) {
f(arr,arr1)
}
val t1 = System.nanoTime()
println("Total Time Consumed:" + ((t1 - t0).toDouble / 1000000000).toDouble + "Seconds")
}
我调用 fun
方法并传递 ES
和 ES1
,如下所示:
fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES , 100000)
fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES1, 100000)
结果表明使用 zipped
的名为 ES1
的方法比使用 zip
的方法 ES
更快。
基于这些观察,我有两个问题。
为什么 zipped
比 zip
快?
在 Scala 中对集合进行逐元素操作是否有更快的方法?
由于 JIT 编译,您应该始终对性能测量保持谨慎,但一个可能的原因是 zipped
懒惰并在 map
期间从原始 Array
vaules 中提取元素调用,而 zip
创建一个新的 Array
对象,然后在新对象上调用 map
。
回答你的第二个问题:
Is there any more faster way to do element wise operation on a collection in Scala?
可悲的事实是,尽管函数式语言简洁明了、提高了生产力并且对错误具有恢复能力,但它不一定是性能最高的。在 OP 的示例中,使用高阶函数定义要针对集合执行的投影会产生开销,而紧密循环会放大这一点。正如其他人指出的那样,为中间结果和最终结果分配额外的存储空间也会产生开销。
如果性能很关键,虽然绝不是普遍的,但在像您这样的情况下,您可以将简洁的函数代码展开回命令式等价物,以便重新获得对内存使用的更直接控制并消除函数调用开销。
在您的具体示例中,zipped
总和可以通过预先分配一个正确大小的固定可变数组强制执行(因为当其中一个集合用完元素时 zip 停止),然后将适当索引处的元素相加(因为按序号索引访问数组元素是一种非常快速的操作)。
例如,将第三个函数 ES3
添加到您的测试套件:
def ES3(arr :Array[Double], arr1 :Array[Double]) :Array[Double] = {
val minSize = math.min(arr.length, arr1.length)
val array = Array.ofDim[Double](minSize)
for (i <- 0 to minSize - 1) {
array(i) = arr(i) + arr1(i)
}
array
}
在我的 i7 上,我得到以下响应时间:
OP ES Total Time Consumed:23.3747857Seconds
OP ES1 Total Time Consumed:11.7506995Seconds
--
ES3 Total Time Consumed:1.0255231Seconds
更令人发指的是对两个数组中较短的数组进行直接就地突变,这显然会破坏这个较短数组的内容,因此只有在不需要原始数组时才应实施来电者的进一步工作:
def ES4(arr :Array[Double], arr1 :Array[Double]) :Array[Double] = {
val minSize = math.min(arr.length, arr1.length)
val array = if (arr.length < arr1.length) arr else arr1
for (i <- 0 to minSize - 1) {
array(i) = arr(i) + arr1(i)
}
array
}
Total Time Consumed:0.3542098Seconds
但显然,直接改变作为参数传递的数组元素不符合 Scala 的精神 - 这段代码有副作用。
老实说,如果您需要在紧密循环中进行这种级别的性能优化,您最好用 Rust、Go 或 C 编写这些算法。
考虑lazyZip
(as lazyZip bs) map { case (a, b) => a + b }
而不是zip
(as zip bs) map { case (a, b) => a + b }
Scala 2.13 added lazyZip
支持 .zipped
Together with .zip
on views, this replaces .zipped
(now
deprecated). (scala/collection-strawman#223)
zipped
(因此 lazyZip
)比 zip
快,因为正如 and Mike Allen 所解释的那样,zip
后跟 map
由于严格性,将导致两个单独的转换,而 zipped
后跟 map
将导致由于惰性而一次性执行单个转换。
zipped
给出 Tuple2Zipped
,并分析 Tuple2Zipped.map
、
class Tuple2Zipped[...](val colls: (It1, It2)) extends ... {
private def coll1 = colls._1
private def coll2 = colls._2
def map[...](f: (El1, El2) => B)(...) = {
val b = bf.newBuilder(coll1)
...
val elems1 = coll1.iterator
val elems2 = coll2.iterator
while (elems1.hasNext && elems2.hasNext) {
b += f(elems1.next(), elems2.next())
}
b.result()
}
我们看到两个集合 coll1
和 coll2
被迭代,并且在每次迭代中传递给 map
的函数 f
被沿途应用
b += f(elems1.next(), elems2.next())
无需分配和转换中间结构。
应用 Travis 的基准测试方法,这里是新的 lazyZip
和弃用的 zipped
之间的比较,其中
@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class ZippedBench {
import scala.collection.mutable._
val as = ArraySeq.fill(10000)(math.random)
val bs = ArraySeq.fill(10000)(math.random)
def lazyZip(as: ArraySeq[Double], bs: ArraySeq[Double]): ArraySeq[Double] =
as.lazyZip(bs).map{ case (a, b) => a + b }
def zipped(as: ArraySeq[Double], bs: ArraySeq[Double]): ArraySeq[Double] =
(as, bs).zipped.map { case (a, b) => a + b }
def lazyZipJavaArray(as: Array[Double], bs: Array[Double]): Array[Double] =
as.lazyZip(bs).map{ case (a, b) => a + b }
@Benchmark def withZipped: ArraySeq[Double] = zipped(as, bs)
@Benchmark def withLazyZip: ArraySeq[Double] = lazyZip(as, bs)
@Benchmark def withLazyZipJavaArray: ArraySeq[Double] = lazyZipJavaArray(as.toArray, bs.toArray)
}
给予
[info] Benchmark Mode Cnt Score Error Units
[info] ZippedBench.withZipped thrpt 20 20197.344 ± 1282.414 ops/s
[info] ZippedBench.withLazyZip thrpt 20 25468.458 ± 2720.860 ops/s
[info] ZippedBench.withLazyZipJavaArray thrpt 20 5215.621 ± 233.270 ops/s
lazyZip
在 ArraySeq
上的表现似乎比 zipped
好一点。有趣的是,注意在 Array
上使用 lazyZip
时性能显着下降。
None 的其他答案提到了速度差异的主要原因,即 zipped
版本避免了 10,000 个元组分配。作为其他几个答案 do 注意,zip
版本涉及一个中间数组,而 zipped
版本没有,但是为 10,000 分配一个数组elements 并不是让 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
版本更具可读性,但我个人认为命令式版本反对 [=] 没有任何意义83=],我会毫不犹豫地自己使用它们。
和列表
更新:我根据另一个答案中的评论向基准添加了 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
这正是我所期望的,因为调用函数本身并不昂贵,而且通过索引访问数组元素非常便宜。
我编写了一些 Scala 代码来对集合执行元素级操作。这里我定义了两个执行相同任务的方法。一种方法使用 zip
,另一种方法使用 zipped
.
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)
为了比较这两种方法的速度,我写了下面的代码:
def fun (arr : Array[Double] , arr1 : Array[Double] , f :(Array[Double],Array[Double]) => Array[Double] , itr : Int) ={
val t0 = System.nanoTime()
for (i <- 1 to itr) {
f(arr,arr1)
}
val t1 = System.nanoTime()
println("Total Time Consumed:" + ((t1 - t0).toDouble / 1000000000).toDouble + "Seconds")
}
我调用 fun
方法并传递 ES
和 ES1
,如下所示:
fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES , 100000)
fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES1, 100000)
结果表明使用 zipped
的名为 ES1
的方法比使用 zip
的方法 ES
更快。
基于这些观察,我有两个问题。
为什么 zipped
比 zip
快?
在 Scala 中对集合进行逐元素操作是否有更快的方法?
由于 JIT 编译,您应该始终对性能测量保持谨慎,但一个可能的原因是 zipped
懒惰并在 map
期间从原始 Array
vaules 中提取元素调用,而 zip
创建一个新的 Array
对象,然后在新对象上调用 map
。
回答你的第二个问题:
Is there any more faster way to do element wise operation on a collection in Scala?
可悲的事实是,尽管函数式语言简洁明了、提高了生产力并且对错误具有恢复能力,但它不一定是性能最高的。在 OP 的示例中,使用高阶函数定义要针对集合执行的投影会产生开销,而紧密循环会放大这一点。正如其他人指出的那样,为中间结果和最终结果分配额外的存储空间也会产生开销。
如果性能很关键,虽然绝不是普遍的,但在像您这样的情况下,您可以将简洁的函数代码展开回命令式等价物,以便重新获得对内存使用的更直接控制并消除函数调用开销。
在您的具体示例中,zipped
总和可以通过预先分配一个正确大小的固定可变数组强制执行(因为当其中一个集合用完元素时 zip 停止),然后将适当索引处的元素相加(因为按序号索引访问数组元素是一种非常快速的操作)。
例如,将第三个函数 ES3
添加到您的测试套件:
def ES3(arr :Array[Double], arr1 :Array[Double]) :Array[Double] = {
val minSize = math.min(arr.length, arr1.length)
val array = Array.ofDim[Double](minSize)
for (i <- 0 to minSize - 1) {
array(i) = arr(i) + arr1(i)
}
array
}
在我的 i7 上,我得到以下响应时间:
OP ES Total Time Consumed:23.3747857Seconds
OP ES1 Total Time Consumed:11.7506995Seconds
--
ES3 Total Time Consumed:1.0255231Seconds
更令人发指的是对两个数组中较短的数组进行直接就地突变,这显然会破坏这个较短数组的内容,因此只有在不需要原始数组时才应实施来电者的进一步工作:
def ES4(arr :Array[Double], arr1 :Array[Double]) :Array[Double] = {
val minSize = math.min(arr.length, arr1.length)
val array = if (arr.length < arr1.length) arr else arr1
for (i <- 0 to minSize - 1) {
array(i) = arr(i) + arr1(i)
}
array
}
Total Time Consumed:0.3542098Seconds
但显然,直接改变作为参数传递的数组元素不符合 Scala 的精神 - 这段代码有副作用。
老实说,如果您需要在紧密循环中进行这种级别的性能优化,您最好用 Rust、Go 或 C 编写这些算法。
考虑lazyZip
(as lazyZip bs) map { case (a, b) => a + b }
而不是zip
(as zip bs) map { case (a, b) => a + b }
Scala 2.13 added lazyZip
支持 .zipped
Together with
.zip
on views, this replaces.zipped
(now deprecated). (scala/collection-strawman#223)
zipped
(因此 lazyZip
)比 zip
快,因为正如 zip
后跟 map
由于严格性,将导致两个单独的转换,而 zipped
后跟 map
将导致由于惰性而一次性执行单个转换。
zipped
给出 Tuple2Zipped
,并分析 Tuple2Zipped.map
、
class Tuple2Zipped[...](val colls: (It1, It2)) extends ... {
private def coll1 = colls._1
private def coll2 = colls._2
def map[...](f: (El1, El2) => B)(...) = {
val b = bf.newBuilder(coll1)
...
val elems1 = coll1.iterator
val elems2 = coll2.iterator
while (elems1.hasNext && elems2.hasNext) {
b += f(elems1.next(), elems2.next())
}
b.result()
}
我们看到两个集合 coll1
和 coll2
被迭代,并且在每次迭代中传递给 map
的函数 f
被沿途应用
b += f(elems1.next(), elems2.next())
无需分配和转换中间结构。
应用 Travis 的基准测试方法,这里是新的 lazyZip
和弃用的 zipped
之间的比较,其中
@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class ZippedBench {
import scala.collection.mutable._
val as = ArraySeq.fill(10000)(math.random)
val bs = ArraySeq.fill(10000)(math.random)
def lazyZip(as: ArraySeq[Double], bs: ArraySeq[Double]): ArraySeq[Double] =
as.lazyZip(bs).map{ case (a, b) => a + b }
def zipped(as: ArraySeq[Double], bs: ArraySeq[Double]): ArraySeq[Double] =
(as, bs).zipped.map { case (a, b) => a + b }
def lazyZipJavaArray(as: Array[Double], bs: Array[Double]): Array[Double] =
as.lazyZip(bs).map{ case (a, b) => a + b }
@Benchmark def withZipped: ArraySeq[Double] = zipped(as, bs)
@Benchmark def withLazyZip: ArraySeq[Double] = lazyZip(as, bs)
@Benchmark def withLazyZipJavaArray: ArraySeq[Double] = lazyZipJavaArray(as.toArray, bs.toArray)
}
给予
[info] Benchmark Mode Cnt Score Error Units
[info] ZippedBench.withZipped thrpt 20 20197.344 ± 1282.414 ops/s
[info] ZippedBench.withLazyZip thrpt 20 25468.458 ± 2720.860 ops/s
[info] ZippedBench.withLazyZipJavaArray thrpt 20 5215.621 ± 233.270 ops/s
lazyZip
在 ArraySeq
上的表现似乎比 zipped
好一点。有趣的是,注意在 Array
上使用 lazyZip
时性能显着下降。
None 的其他答案提到了速度差异的主要原因,即 zipped
版本避免了 10,000 个元组分配。作为其他几个答案 do 注意,zip
版本涉及一个中间数组,而 zipped
版本没有,但是为 10,000 分配一个数组elements 并不是让 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
版本更具可读性,但我个人认为命令式版本反对 [=] 没有任何意义83=],我会毫不犹豫地自己使用它们。
和列表
更新:我根据另一个答案中的评论向基准添加了 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
这正是我所期望的,因为调用函数本身并不昂贵,而且通过索引访问数组元素非常便宜。