Żadna z pozostałych odpowiedzi nie wspomina o głównej przyczynie różnicy prędkości, która polega na tym, że zippedwersja unika 10.000 alokacji krotek. Jak kilka innych odpowiedzi zrobić noty, zipwersja obejmuje pośrednią tablicę, natomiast zippedwersja nie robi, ale przydzielanie tablicę do 10000 elementów nie jest to, co sprawia, że zipwersja o wiele gorsze jest to 10,000 krótkotrwałe krotki że są umieszczane w tej tablicy. Są one reprezentowane przez obiekty w JVM, więc wykonujesz kilka przydziałów obiektów dla rzeczy, które natychmiast zamierzasz wyrzucić.
Reszta tej odpowiedzi zawiera tylko trochę więcej szczegółów na temat tego, jak możesz to potwierdzić.
Lepsze testy porównawcze
Naprawdę chcesz używać frameworku, takiego jak jmh, do odpowiedzialnego testowania wydajności w JVM, a nawet wtedy odpowiedzialna część jest trudna, chociaż samo skonfigurowanie jmh nie jest takie złe. Jeśli masz coś project/plugins.sbttakiego:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
I build.sbtpodobne (używam 2.11.8, ponieważ wspominasz, że tego właśnie używasz):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
Następnie możesz napisać swój test porównawczy w następujący sposób:
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)
}
I uruchom z 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
Co pokazuje, że zippedwersja uzyskuje około 80% większą przepustowość, co prawdopodobnie jest mniej więcej takie samo jak twoje pomiary.
Pomiar przydziałów
Możesz także poprosić jmh o pomiar przydziałów za pomocą -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
… Gdzie gc.alloc.rate.normjest prawdopodobnie najciekawsza część, pokazująca, że zipwersja przydziela ponad trzy razy więcej zipped.
Konieczne wdrożenia
Gdybym wiedział, że ta metoda będzie wywoływana w kontekstach bardzo wrażliwych na wydajność, prawdopodobnie zastosowałbym ją w następujący sposób:
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
}
Zauważ, że w przeciwieństwie do wersji zoptymalizowanej w jednej z pozostałych odpowiedzi, ta funkcja jest używana whilezamiast, forponieważ fornadal będzie się przełączać do operacji kolekcji Scala. Możemy porównać tę implementację ( withWhile), zoptymalizowaną (ale nie na miejscu) implementację drugiej odpowiedzi ( withFor) oraz dwie oryginalne implementacje:
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
To naprawdę ogromna różnica między wersjami imperatywną i funkcjonalną, a wszystkie sygnatury metod są dokładnie identyczne, a implementacje mają tę samą semantykę. To nie jest tak, że implementacje imperatywne używają stanu globalnego itp. Chociaż wersje zipi zippedsą bardziej czytelne, osobiście nie sądzę, by istniał sens, w którym wersje imperatywne są sprzeczne z „duchem Scali”, i nie zawahałbym się korzystać z nich osobiście.
Z tabelą
Aktualizacja: Dodałem tabulateimplementację do testu porównawczego na podstawie komentarza w innej odpowiedzi:
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))
}
Jest znacznie szybszy niż zipwersje, choć wciąż znacznie wolniejszy niż te niezbędne:
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
Tego się spodziewałem, ponieważ wywoływanie funkcji nie jest z natury drogie, a dostęp do elementów tablicy za pomocą indeksu jest bardzo tani.