他の回答はどれも速度の違いの主な理由について言及していません。これは、zipped
バージョンが10,000のタプル割り当てを回避するということです。他のいくつかの回答が指摘しているように、zip
バージョンには中間配列が含まれますが、バージョンには含まれzipped
ませんが、10,000要素に配列を割り当てることでzip
バージョンが大幅に悪化するわけではありません。その配列に入れられています。これらは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
バージョンが3倍以上割り当てられていることを示してい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のコレクションの操作にdesugarます。この実装(withWhile
)、他の回答の最適化(インプレースではない)実装(withFor
)、および2つの元の実装を比較できます。
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
これは、関数の呼び出しに本質的にコストがかかることはなく、インデックスによる配列要素へのアクセスは非常に安価であるため、私が期待することです。