Keine der anderen Antworten erwähnt den Hauptgrund für den Geschwindigkeitsunterschied, nämlich dass die zippedVersion 10.000 Tupelzuweisungen vermeidet. Als ein paar der anderen Antworten tun Note, die zipbeinhaltet Version eine Zwischen Array, während die zippedVersion nicht der Fall ist, sondern auch für 10.000 Elemente eines Arrays Zuteilung ist nicht das, was die macht zipVersion 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.sbtsolche haben:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
Und build.sbtso 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 zippedVersion 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.normist wahrscheinlich der interessanteste Teil, der zeigt, dass die zipVersion 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 whileanstelle von a verwendet wird, forda der forWille 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 zipund 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 tabulatehabe 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 zipVersionen, 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.