Aucune des autres réponses ne mentionne la principale raison de la différence de vitesse, à savoir que la zipped
version évite 10 000 attributions de tuple. En tant que deux autres réponses ne note, la zip
Version implique un tableau intermédiaire, alors que la zipped
version ne pas, mais l' attribution d' un tableau pour 10.000 éléments ne sont pas ce qui fait la la zip
version tellement pire , ce sont les 10.000 tuples vécu court que sont mis dans ce tableau. Celles-ci sont représentées par des objets sur la JVM, donc vous faites un tas d'allocations d'objets pour des choses que vous allez immédiatement jeter.
Le reste de cette réponse va juste dans un peu plus de détails sur la façon dont vous pouvez le confirmer.
Meilleure analyse comparative
Vous voulez vraiment utiliser un framework comme jmh pour effectuer n'importe quel type d'analyse comparative de manière responsable sur la JVM, et même dans ce cas, la partie responsable est difficile, bien que la configuration de jmh lui-même ne soit pas trop mauvaise. Si vous en avez un project/plugins.sbt
comme ça:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
Et un build.sbt
comme ça (j'utilise 2.11.8 puisque vous mentionnez que c'est ce que vous utilisez):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
Ensuite, vous pouvez écrire votre référence comme ceci:
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)
}
Et lancez-le avec 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
Ce qui montre que la zipped
version obtient environ 80% de débit en plus, ce qui est probablement plus ou moins le même que vos mesures.
Mesurer les allocations
Vous pouvez également demander à jmh de mesurer les allocations avec -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
… Où gc.alloc.rate.norm
est probablement la partie la plus intéressante, montrant que la zip
version alloue plus de trois fois plus zipped
.
Implémentations impératives
Si je savais que cette méthode allait être appelée dans des contextes extrêmement sensibles aux performances, je l'implémenterais probablement comme ceci:
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
}
Notez que contrairement à la version optimisée dans l'une des autres réponses, celle-ci utilise à la while
place d'un for
car la for
va toujours se dissiper dans les opérations de collections Scala. Nous pouvons comparer cette implémentation ( withWhile
), l' implémentation optimisée (mais pas en place) de l'autre réponse ( withFor
), et les deux implémentations originales:
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
C'est une énorme différence entre les versions impérative et fonctionnelle, et toutes ces signatures de méthode sont exactement identiques et les implémentations ont la même sémantique. Ce n'est pas comme si les implémentations impératives utilisaient l'état global, etc. Bien que les versions zip
et zipped
soient plus lisibles, personnellement, je ne pense pas qu'il y ait un sens dans lequel les versions impératives sont contre "l'esprit de Scala", et je n'hésiterais pas de les utiliser moi-même.
Avec tabuler
Mise à jour: j'ai ajouté une tabulate
implémentation à la référence basée sur un commentaire dans une autre réponse:
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))
}
C'est beaucoup plus rapide que les zip
versions, bien que beaucoup plus lent que les impératifs:
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
C'est ce à quoi je m'attendrais, car il n'y a rien de intrinsèquement coûteux à appeler une fonction, et parce que l'accès aux éléments de tableau par index est très bon marché.