Tidak ada jawaban lain yang menyebutkan alasan utama perbedaan kecepatan, yaitu zipped
versi menghindari 10.000 alokasi tuple. Sebagai beberapa jawaban yang lain melakukan catatan, zip
versi melibatkan array menengah, sedangkan zipped
versi tidak, tapi mengalokasikan sebuah array untuk 10.000 elemen tidak apa yang membuat zip
versi jauh lebih buruk-itu 10.000 tupel berumur pendek yang sedang dimasukkan ke dalam array itu. Ini diwakili oleh objek pada JVM, jadi Anda melakukan banyak alokasi objek untuk hal-hal yang akan segera Anda buang.
Sisa dari jawaban ini hanya menjelaskan sedikit lebih detail tentang bagaimana Anda dapat mengonfirmasi hal ini.
Pembandingan yang lebih baik
Anda benar-benar ingin menggunakan kerangka kerja seperti jmh untuk melakukan pembandingan apa pun secara bertanggung jawab pada JVM, dan bahkan bagian yang bertanggung jawab itu sulit, meskipun mengatur jmh itu sendiri tidak terlalu buruk. Jika Anda memiliki yang project/plugins.sbt
seperti ini:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
Dan build.sbt
seperti ini (saya menggunakan 2.11.8 karena Anda menyebutkan itu yang Anda gunakan):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
Maka Anda dapat menulis patokan Anda seperti ini:
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)
}
Dan jalankan dengan 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
Yang menunjukkan bahwa zipped
versi tersebut mendapatkan throughput sekitar 80% lebih banyak, yang mungkin kurang lebih sama dengan pengukuran Anda.
Mengukur alokasi
Anda juga dapat meminta jmh untuk mengukur alokasi dengan -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
... di mana gc.alloc.rate.norm
mungkin bagian yang paling menarik, menunjukkan bahwa zip
versi tersebut mengalokasikan lebih dari tiga kali lipat zipped
.
Implementasi imperatif
Jika saya tahu bahwa metode ini akan dipanggil dalam konteks yang sangat sensitif terhadap kinerja, saya mungkin akan menerapkannya seperti ini:
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
}
Perhatikan bahwa tidak seperti versi yang dioptimalkan dalam salah satu jawaban lain, ini menggunakan while
alih-alih for
karena for
akan tetap masuk ke operasi koleksi Scala. Kita dapat membandingkan implementasi ini ( withWhile
), implementasi yang lain dioptimalkan (tetapi bukan di tempat) ( withFor
), dan dua implementasi asli:
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
Itu perbedaan yang sangat besar antara versi imperatif dan fungsional, dan semua tanda tangan metode ini persis identik dan implementasinya memiliki semantik yang sama. Ini tidak seperti implementasi imperatif menggunakan negara global, dll. Sementara zip
dan zipped
versi lebih mudah dibaca, saya pribadi tidak berpikir ada arti di mana versi imperatif menentang "semangat Scala", dan saya tidak akan ragu untuk menggunakannya sendiri.
Dengan tabulasi
Pembaruan: Saya menambahkan tabulate
implementasi ke tolok ukur berdasarkan komentar di jawaban lain:
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))
}
Ini jauh lebih cepat daripada zip
versi, meskipun masih jauh lebih lambat daripada versi imperatif:
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
Inilah yang saya harapkan, karena tidak ada yang secara inheren mahal untuk memanggil fungsi, dan karena mengakses elemen array dengan indeks sangat murah.