Keine der anderen Antworten erwähnt den Hauptgrund für den Geschwindigkeitsunterschied, nämlich dass die zipped
Version 10.000 Tupelzuweisungen vermeidet. Als ein paar der anderen Antworten tun Note, die zip
beinhaltet Version eine Zwischen Array, während die zipped
Version nicht der Fall ist, sondern auch für 10.000 Elemente eines Arrays Zuteilung ist nicht das, was die macht zip
Version so viel schlechter es die 10.000 kurzlebig Tupel ist das werden in dieses Array eingefügt. Diese werden durch Objekte in der JVM dargestellt, sodass Sie eine Reihe von Objektzuordnungen für Dinge vornehmen, die Sie sofort wegwerfen werden.
Der Rest dieser Antwort geht nur etwas detaillierter darauf ein, wie Sie dies bestätigen können.
Besseres Benchmarking
Sie möchten wirklich ein Framework wie jmh verwenden , um verantwortungsbewusstes Benchmarking für die JVM durchzuführen , und selbst dann ist der verantwortungsvolle Teil schwierig, obwohl das Einrichten von jmh selbst nicht schlecht ist. Wenn Sie eine project/plugins.sbt
solche haben:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
Und build.sbt
so etwas (ich verwende 2.11.8, da Sie erwähnen, dass Sie das verwenden):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
Dann können Sie Ihren Benchmark folgendermaßen schreiben:
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)
}
Und führen Sie es aus mit 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
Dies zeigt, dass die zipped
Version etwa 80% mehr Durchsatz erzielt, was wahrscheinlich mehr oder weniger Ihren Messungen entspricht.
Zuordnungen messen
Sie können jmh auch bitten, Zuordnungen zu messen mit -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
… Wo gc.alloc.rate.norm
ist wahrscheinlich der interessanteste Teil, der zeigt, dass die zip
Version mehr als dreimal so viel zuweist wie zipped
.
Imperative Implementierungen
Wenn ich wüsste, dass diese Methode in extrem leistungsabhängigen Kontexten aufgerufen werden würde, würde ich sie wahrscheinlich folgendermaßen implementieren:
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
}
Beachten Sie, dass im Gegensatz zur optimierten Version in einer der anderen Antworten while
anstelle von a verwendet wird, for
da der for
Wille weiterhin in Scala-Sammlungsvorgängen enthalten ist. Wir können diese Implementierung ( withWhile
), die optimierte (aber nicht vorhandene) Implementierung ( withFor
) der anderen Antwort ( ) und die beiden ursprünglichen Implementierungen vergleichen:
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
Das ist ein wirklich großer Unterschied zwischen der imperativen und der funktionalen Version, und alle diese Methodensignaturen sind genau identisch und die Implementierungen haben dieselbe Semantik. Es ist nicht so, dass die imperativen Implementierungen den globalen Status usw. verwenden. Obwohl die zip
und zipped
-Versionen besser lesbar sind, glaube ich persönlich nicht, dass die imperativen Versionen in irgendeiner Weise gegen den "Geist von Scala" sind, und ich würde nicht zögern sie selbst zu benutzen.
Mit tabellarisch
Update: Ich tabulate
habe dem Benchmark eine Implementierung hinzugefügt , die auf einem Kommentar in einer anderen Antwort basiert:
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))
}
Es ist viel schneller als die zip
Versionen, obwohl immer noch viel langsamer als die zwingenden:
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
Dies ist, was ich erwarten würde, da das Aufrufen einer Funktion nicht von Natur aus teuer ist und der Zugriff auf Array-Elemente über den Index sehr billig ist.