Nessuna delle altre risposte menziona il motivo principale della differenza di velocità, ovvero che la zipped
versione evita 10.000 allocazioni di tuple. Come una coppia delle altre risposte fanno notare, la zip
versione comporta una serie intermedia, mentre la zipped
versione non, ma allocare una matrice per 10.000 elementi che non è ciò che rende la zip
versione 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.sbt
simile:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
E un build.sbt
simile (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 zipped
versione 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 zip
versione è 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 while
invece di un for
poiché la for
volontà 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 zip
e zipped
sono 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 tabulate
un'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 zip
versioni, 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.