Nessuna delle altre risposte menziona il motivo principale della differenza di velocità, ovvero che la zippedversione evita 10.000 allocazioni di tuple. Come una coppia delle altre risposte fanno notare, la zipversione comporta una serie intermedia, mentre la zippedversione non, ma allocare una matrice per 10.000 elementi che non è ciò che rende la zipversione in modo molto peggio: è 10.000 tuple di breve durata che vengono inseriti in quell'array. Questi sono rappresentati da oggetti sulla JVM, quindi stai facendo un sacco di allocazioni di oggetti per cose che immediatamente butterai via.
Il resto di questa risposta fornisce solo qualche dettaglio in più su come confermarlo.
Migliore benchmarking
Volete davvero usare un framework come jmh per fare qualsiasi tipo di benchmarking responsabilmente sulla JVM, e anche in questo caso la parte responsabile è difficile, anche se configurare jmh in sé non è poi così male. Se hai un project/plugins.sbtsimile:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
E un build.sbtsimile (sto usando 2.11.8 poiché dici che è quello che stai usando):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
Quindi puoi scrivere il tuo benchmark in questo modo:
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)
}
Ed eseguilo con 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
Ciò dimostra che la zippedversione ottiene circa l'80% in più di throughput, che è probabilmente più o meno la stessa delle tue misurazioni.
Misurazione delle allocazioni
Puoi anche chiedere a jmh di misurare le allocazioni con -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
... dove gc.alloc.rate.normè probabilmente la parte più interessante, a dimostrazione del fatto che la zipversione è allocata oltre tre volte tanto zipped.
Implementazioni imperative
Se sapessi che questo metodo sarebbe stato chiamato in contesti estremamente sensibili alle prestazioni, probabilmente lo implementerei in questo modo:
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
}
Si noti che a differenza della versione ottimizzata in una delle altre risposte, questo utilizza whileinvece di un forpoiché la forvolontà sarà ancora desugar nelle operazioni delle collezioni Scala. Possiamo confrontare questa implementazione ( withWhile), l' implementazione ottimizzata (ma non sul posto) dell'altra risposta ( withFor) e le due implementazioni originali:
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
Questa è una differenza davvero enorme tra le versioni imperative e funzionali, e tutte queste firme dei metodi sono esattamente identiche e le implementazioni hanno la stessa semantica. Non è che le implementazioni imperative stiano usando lo stato globale, ecc. Mentre le versioni zipe zippedsono più leggibili, personalmente non penso che ci sia alcun senso in cui le versioni imperative siano contro lo "spirito di Scala", e non esiterei per usarli da solo.
Con tabulato
Aggiornamento: ho aggiunto tabulateun'implementazione al benchmark sulla base di un commento in un'altra risposta:
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))
}
È molto più veloce delle zipversioni, anche se ancora molto più lento di quelle imperative:
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
Questo è quello che mi aspetterei, dal momento che non c'è nulla di intrinsecamente costoso nel chiamare una funzione e perché l'accesso agli elementi dell'array per indice è molto economico.