Żadna z pozostałych odpowiedzi nie wspomina o głównej przyczynie różnicy prędkości, która polega na tym, że zipped
wersja unika 10.000 alokacji krotek. Jak kilka innych odpowiedzi zrobić noty, zip
wersja obejmuje pośrednią tablicę, natomiast zipped
wersja nie robi, ale przydzielanie tablicę do 10000 elementów nie jest to, co sprawia, że zip
wersja 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.sbt
takiego:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
I build.sbt
podobne (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 zipped
wersja 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.norm
jest prawdopodobnie najciekawsza część, pokazująca, że zip
wersja 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 while
zamiast, for
ponieważ for
nadal 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 zip
i zipped
są 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 tabulate
implementację 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ż zip
wersje, 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.