Жодна з інших відповідей не згадує первинну причину різниці у швидкості, яка полягає в тому, що zipped
версія уникає 10 000 кортежів. Як пара інших відповідей зробити примітку, то zip
версія включає в себе проміжний масив, в той час як zipped
версія не робить, але виділяти масив 10000 елементів не те , що робить zip
версію набагато гірше, це 10.000 короткоживучих кортежів , які вводяться в цей масив. Вони представлені об'єктами на JVM, тому ви робите купу виділень об'єктів для речей, які ви негайно збираєтеся викинути.
У решті цієї відповіді якраз детальніше йдеться про те, як ви можете це підтвердити.
Краще тестування
Ви дійсно хочете використовувати таку структуру, як jmh, щоб робити будь-який вид бенчмаркингу відповідально на JVM, і навіть тоді відповідальна частина складна, хоча налаштування jmh сама по собі не дуже погана. Якщо у вас є project/plugins.sbt
такий:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
І build.sbt
подібне (я використовую 2.11.8, оскільки ви згадуєте саме те, що ви використовуєте):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
Тоді ви можете написати свій орієнтир так:
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)
}
І запустіть його 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
Що свідчить про те, що zipped
версія отримує приблизно на 80% більше пропускної здатності, що, мабуть, більш-менш збігається з вашими вимірами.
Вимірювання асигнувань
Ви також можете попросити jmh для вимірювання асигнувань за допомогою -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
… Де gc.alloc.rate.norm
, мабуть, найцікавіша частина, що показує, що zip
версія виділяє понад три рази більше zipped
.
Імперативні реалізації
Якби я знав, що цей метод буде називатися в надзвичайно чутливих до продуктивності контекстах, я, мабуть, реалізував би його так:
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
}
Зауважте, що на відміну від оптимізованої версії в одній з інших відповідей, вона використовує while
замість того, for
що for
воля все ще дегуртується в операції зі збирання Scala. Ми можемо порівняти цю реалізацію ( withWhile
), оптимізовану (але не за місцем) реалізацію відповіді ( withFor
), а також дві оригінальні реалізації:
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
Це дійсно величезна різниця між імперативною та функціональною версіями, і всі ці підписи методів точно ідентичні, і реалізації мають однакову семантику. Це не так, як імперативні реалізації використовує глобальний стан, і т.д. У той час як zip
і zipped
версія більш читається, особисто я не думаю , що є якийсь -то сенс , в якому імперативні версії проти «духу Scala», і я б не вагаючись , використовувати їх сам.
З табличними таблицями
Оновлення: я додав tabulate
виконання до еталону на основі коментаря в іншій відповіді:
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))
}
Це набагато швидше, ніж zip
версії, хоча все ж набагато повільніше, ніж імперативні:
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
Це те, що я очікував, оскільки нічого не є суттєво дорогим у виклику функції, і тому що доступ до елементів масиву за індексом дуже дешевий.