Scala'da neden zip'ten daha hızlı sıkıştırılıyor?


38

Bir koleksiyon üzerinde element bazında bir işlem gerçekleştirmek için bazı Scala kodu yazdım. Burada aynı görevi gerçekleştiren iki yöntem tanımladım. Bir yöntem kullanır zip, diğeri kullanır zipped.

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)

Bu iki yöntemi hız açısından karşılaştırmak için aşağıdaki kodu yazdım:

def fun (arr : Array[Double] , arr1 : Array[Double] , f :(Array[Double],Array[Double]) => Array[Double] , itr : Int) ={
  val t0 = System.nanoTime()
  for (i <- 1 to itr) {
       f(arr,arr1)
       }
  val t1 = System.nanoTime()
  println("Total Time Consumed:" + ((t1 - t0).toDouble / 1000000000).toDouble + "Seconds")
}

Ben funyöntemi çağırmak ve geçmek ESve ES1aşağıdaki gibi:

fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES , 100000)
fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES1, 100000)

Sonuçlar adlı yöntem olduğunu göstermektedir ES1o kullanımlarını zippeddaha hızlı yöntemle daha ESthat use zip. Bu gözlemlere dayanarak iki sorum var.

Neden daha zippedhızlı zip?

Scala'da bir koleksiyon üzerinde element-operasyonlar yapmanın daha hızlı bir yolu var mı?



8
Çünkü JIT, 'eğlenceli' çağrıldığında ikinci kez daha agresif bir şekilde optimize etmeye karar verdi. Veya GC, ES çalışırken bir şeyi temizlemeye karar verdiği için. Veya işletim sisteminiz ES testiniz devam ederken daha iyi işlerin yapılmasına karar verdiği için. Bir şey olabilir, bu mikrobenchmark kesin değil.
Andrey Tyukin

1
Makinenizde sonuçlar nelerdir? Ne kadar hızlı?
Peeyush Kushwaha

Aynı popülasyon boyutu ve yapılandırmaları için Zip
sıkıştırması

3
Sonuçlarınız anlamsız. Mikro ölçütler yapmanız gerekiyorsa JMH kullanın .
OrangeDog

Yanıtlar:


17

İkinci sorunuza cevap vermek için:

Scala'da bir koleksiyon üzerinde element bilge operasyon yapmanın daha hızlı bir yolu var mı?

Üzücü gerçek şu ki, özlü olmasına, iyileştirilmiş üretkenliğe ve işlevsel dillerin mutlaka en yüksek performansa sahip olmadığı hatalarına karşı dirençli olmasına rağmen - serbest olmayan koleksiyonlara karşı yürütülecek bir projeksiyonu tanımlamak için daha yüksek dereceli işlevler kullanmak ve sıkı döngünüz bunu vurgular. Diğerlerinin de belirttiği gibi, ara ve nihai sonuçlar için ek depolama alanı tahsisi de ek yüke sahip olacaktır.

Performans kritik ise, hiçbir şekilde evrensel olmasa da, sizin gibi durumlarda, bellek kullanımı ve işlev çağrılarını ortadan kaldırmak için Scala'nın işlemlerini zorunlu eşdeğerlere dönüştürebilirsiniz.

Özel örneğinizde, zippedtoplamlar zorunlu, doğru boyutta sabit, değişken bir dizi önceden tahsis edilerek (koleksiyonlardan biri öğeler bittiğinde zip durduğundan) ve ardından uygun dizine birlikte öğeler ekleyerek (erişime başladıktan sonra) zorunlu olarak gerçekleştirilebilir. sıralı dizine göre dizi elemanları çok hızlı bir işlemdir).

ES3Test takımınıza üçüncü bir işlev ekleme :

def ES3(arr :Array[Double], arr1 :Array[Double]) :Array[Double] = {
   val minSize = math.min(arr.length, arr1.length)
   val array = Array.ofDim[Double](minSize)
   for (i <- 0 to minSize - 1) {
     array(i) = arr(i) + arr1(i)
   }
  array
}

İ7'de aşağıdaki yanıt sürelerini alıyorum:

OP ES Total Time Consumed:23.3747857Seconds
OP ES1 Total Time Consumed:11.7506995Seconds
--
ES3 Total Time Consumed:1.0255231Seconds

Daha da hevesli, dizilerden birinin içeriğini açıkça bozacak ve sadece orijinal diziye tekrar ihtiyaç duyulmayacaksa yapılacak iki diziden daha kısa olanının yerinde yerinde mutasyonu yapmak olacaktır:

def ES4(arr :Array[Double], arr1 :Array[Double]) :Array[Double] = {
   val minSize = math.min(arr.length, arr1.length)
   val array = if (arr.length < arr1.length) arr else arr1
   for (i <- 0 to minSize - 1) {
      array(i) = arr(i) + arr1(i)
   }
  array
}

Total Time Consumed:0.3542098Seconds

Fakat belli ki, dizi öğelerinin doğrudan mutasyonu Scala'nın ruhunda değil.


2
Yukarıdaki kodumda paralel bir şey yok. Bu özel sorun paralelleştirilebilir olmasına rağmen (dizilerin farklı bölümlerinde birden çok iş parçacığı çalışabileceğinden), yalnızca 10k öğelerinde bu kadar basit bir işlemde çok fazla nokta olmaz - yeni iş parçacıkları oluşturma ve senkronize etme yükü muhtemelen herhangi bir faydadan daha ağır basacaktır. . Dürüst olmak gerekirse, bu performans optimizasyonu düzeyine ihtiyacınız varsa, Rust, Go veya C'de bu tür algoritmaları yazmanız daha iyi olur
StuartLC

3
Array.tabulate(minSize)(i => arr(i) + arr1(i))
Dizinizi

1
@SarveshKumarSingh bu çok daha yavaş. Neredeyse 9 saniye sürer
user12140540

1
Array.tabulateburada zipveya zippedburadakinden çok daha hızlı olmalı (ve benim ölçütlerimde).
Travis Brown

1
@StuartLC "Performans, yalnızca daha üst düzey işlev bir şekilde paketlenmemiş ve satır içi ise eşdeğer olur." Bu gerçekten doğru değil. Hatta fordaha yüksek dereceden fonksiyon çağrısına şekeri alınmış is ( foreach). Lambda her iki durumda da sadece bir kez başlatılacaktır.
Travis Brown

50

Diğer cevapların hiçbiri, hız farkının birincil nedeninden bahsetmemektedir, bu da zippedsürümün 10.000 tuple tahsisinden kaçınmasıdır. Diğer cevaplar bir çift olarak yapmak notu zipise versiyon, bir ara dizi içerir zippedsürüm değil, ama 10.000 öğeleri için bir dizi tahsis kılan değil zipversiyonunu çok daha kötü-10.000 kısa ömürlü tuples var olduğunu bu diziye konuluyor. Bunlar JVM üzerindeki nesneler tarafından temsil edilir, bu yüzden hemen atacağınız şeyler için bir grup nesne tahsisi yapıyorsunuz.

Bu cevabın geri kalanı bunu nasıl doğrulayabileceğiniz hakkında biraz daha ayrıntıya giriyor.

Daha iyi kıyaslama

JVM'de sorumlu bir şekilde herhangi bir kıyaslama yapmak için jmh gibi bir çerçeve kullanmak istiyorsunuz ve o zaman bile jmh'yi kurmak çok kötü olmasa da sorumlu kısım zor. Eğer böyle bir şey varsa project/plugins.sbt:

addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")

Ve bunun build.sbtgibi (kullandığınız şeyden bahsettiğinizden 2.11.8 kullanıyorum):

scalaVersion := "2.11.8"

enablePlugins(JmhPlugin)

Ardından kıstasınızı şu şekilde yazabilirsiniz:

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)
}

Ve şununla çalıştır 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

Bu, zippedsürümün yaklaşık% 80 daha fazla verim aldığını gösterir , bu da muhtemelen ölçümlerinizle aynı veya daha az aynıdır.

Tahsislerin ölçülmesi

Ayrıca jmh'den ayırmaları ölçmesini isteyebilirsiniz -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.normMuhtemelen en ilginç kısım nerede?zip versiyonun üç katından fazla tahsis edildiğini gösteriyor zipped.

Zorunlu uygulamalar

Bu yöntemin son derece performansa duyarlı bağlamlarda çağrılacağını bilseydim, muhtemelen şu şekilde uygularım:

  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
  }

Diğer yanıtları birinde optimize sürümü, aksine bu kullandığını unutmayın whileyerine ait forberi forhala Scala koleksiyonları operasyonları içine desugar edecektir. Bu uygulamayı ( withWhile), diğer yanıtın optimize edilmiş (ancak yerinde değil) uygulamasını ( withFor) ve iki orijinal uygulamayı karşılaştırabiliriz:

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

Bu, zorunlu ve işlevsel sürümler arasında gerçekten büyük bir farktır ve tüm bu yöntem imzaları tamamen aynıdır ve uygulamalar aynı semantiğe sahiptir. Zorunlu uygulamalar küresel devlet vb. Kullanıyor gibi değil.zip Ve zippedsürümleri daha okunaklı , kişisel olarak zorunlu sürümlerin "Scala ruhuna" karşı bir anlamı olduğunu düşünmüyorum ve tereddüt etmem onları kendim kullanmak için.

Tablo ile

Güncelleme: Başka bir tabulateyanıttaki yoruma dayanarak karşılaştırmaya bir uygulama ekledim :

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))
}

zipZorunlu olanlardan çok daha yavaş olmasına rağmen , sürümlerden çok daha hızlı :

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

Bir işlevi çağırmanın doğası gereği pahalı bir şey olmadığı ve dizi öğelerine dizinle erişilmesi çok ucuz olduğu için beklediğim şey budur.


8

Düşünmek lazyZip

(as lazyZip bs) map { case (a, b) => a + b }

onun yerine zip

(as zip bs) map { case (a, b) => a + b }

Scala 2.13 lehine eklendi lazyZip.zipped

.zipGörüşlerle birlikte , bu .zippedartık değiştirildi (şimdi kullanımdan kaldırıldı). ( scala / koleksiyon-strawman # 223 )

zipped(ve dolayısıyla lazyZip) daha hızlıdır ziptarafından açıklandığı gibi, çünkü Tim ve Mike Allen , zipardından mapbağlı katılığı iki ayrı dönüşümler neden olur iken, zippedbunu takibenmap bağlı laziness bir seferde yürütülen tek bir dönüşüm ile sonuçlanır.

zippedverir Tuple2Zippedve analiz eder Tuple2Zipped.map,

class Tuple2Zipped[...](val colls: (It1, It2)) extends ... {
  private def coll1 = colls._1
  private def coll2 = colls._2

  def map[...](f: (El1, El2) => B)(...) = {
    val b = bf.newBuilder(coll1)
    ...
    val elems1 = coll1.iterator
    val elems2 = coll2.iterator

    while (elems1.hasNext && elems2.hasNext) {
      b += f(elems1.next(), elems2.next())
    }

    b.result()
  }

iki koleksiyonları görmek coll1ve coll2üzerinde tekrarlanır ve her tekrarında işlev fgeçirilen mapyol boyunca uygulanır

b += f(elems1.next(), elems2.next())

aracı yapıları tahsis etmek ve dönüştürmek zorunda kalmadan.


Travis uygulanması metodoloji esas, burada yeni arasında bir karşılaştırma bulunmaktadır lazyZipve kullanımdan kaldırıldı zippednerede

@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class ZippedBench {
  import scala.collection.mutable._
  val as = ArraySeq.fill(10000)(math.random)
  val bs = ArraySeq.fill(10000)(math.random)

  def lazyZip(as: ArraySeq[Double], bs: ArraySeq[Double]): ArraySeq[Double] =
    as.lazyZip(bs).map{ case (a, b) => a + b }

  def zipped(as: ArraySeq[Double], bs: ArraySeq[Double]): ArraySeq[Double] =
    (as, bs).zipped.map { case (a, b) => a + b }

  def lazyZipJavaArray(as: Array[Double], bs: Array[Double]): Array[Double] =
    as.lazyZip(bs).map{ case (a, b) => a + b }

  @Benchmark def withZipped: ArraySeq[Double] = zipped(as, bs)
  @Benchmark def withLazyZip: ArraySeq[Double] = lazyZip(as, bs)
  @Benchmark def withLazyZipJavaArray: ArraySeq[Double] = lazyZipJavaArray(as.toArray, bs.toArray)
}

verir

[info] Benchmark                          Mode  Cnt      Score      Error  Units
[info] ZippedBench.withZipped            thrpt   20  20197.344 ± 1282.414  ops/s
[info] ZippedBench.withLazyZip           thrpt   20  25468.458 ± 2720.860  ops/s
[info] ZippedBench.withLazyZipJavaArray  thrpt   20   5215.621 ±  233.270  ops/s

lazyZipBiraz daha iyi performans görünüyor zippedüzerinde ArraySeq. Kullanırken İlginçtir, önemli ölçüde bozulmuş performansı fark lazyZipüzerinde Array.


lazyZip Scala 2.13.1 sürümünde mevcuttur. Şu anda Scala 2.11.8 kullanıyorum
user12140540

5

JIT derlemesi nedeniyle her zaman performans ölçümü konusunda dikkatli olmalısınız, ancak olası bir neden zippedtembeldir ve çağrı Arraysırasında orijinal vaüllerden elemanları çıkarırken yeni bir nesne oluşturur ve daha sonra yeni nesneyi çağırır .mapzipArraymap

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.