स्काला में ज़िप की तुलना में तेजी से ज़िप क्यों किया जाता है?


38

मैंने एक संग्रह पर एक तत्व-वार ऑपरेशन करने के लिए कुछ स्काला कोड लिखा है। यहाँ मैंने दो विधियाँ परिभाषित की हैं जो समान कार्य करती हैं। एक विधि का उपयोग करता है zipऔर दूसरा उपयोग करता है 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)

गति के संदर्भ में इन दो विधियों की तुलना करने के लिए, मैंने निम्नलिखित कोड लिखा:

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

मैं funविधि कहता हूं ESऔर ES1नीचे और पास के रूप में:

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)

परिणाम बताते हैं कि ES1उपयोग zippedकी जाने वाली विधि ES, उपयोग की जाने वाली विधि से तेज है zip। इन टिप्पणियों के आधार पर, मेरे दो प्रश्न हैं।

से zippedतेज क्यों है zip?

क्या स्केला में संग्रह पर तत्व-वार संचालन करने का कोई तेज़ तरीका है?



8
क्योंकि जेआईटी ने दूसरी बार आक्रामक तरीके से अनुकूलन करने का फैसला किया, जिसमें 'मस्ती' को देखा गया। या क्योंकि ईएस चल रहा था, तो जीसी ने कुछ साफ करने का फैसला किया। या क्योंकि आपके ऑपरेटिंग सिस्टम ने फैसला किया है कि आपके ES परीक्षण के चलने के दौरान इसके लिए बेहतर चीजें थीं। कुछ भी हो सकता है, यह माइक्रोबैनमार्क केवल निर्णायक नहीं है।
एंड्रे टायुकिन

1
आपकी मशीन पर परिणाम क्या हैं? कितना तेज?
पीयूष कुशवाहा

समान जनसंख्या आकार और कॉन्फ़िगरेशन के लिए, ज़िप्ड को 32 सेकंड का समय लगता है जबकि ज़िप को 44 सेकंड का समय लगता है
user12140540

3
आपके परिणाम निरर्थक हैं। यदि आप माइक्रो-बेंचमार्क करना चाहते हैं तो JMH का उपयोग करें ।
ऑरेंजडॉग

जवाबों:


17

अपने दूसरे प्रश्न का उत्तर देने के लिए:

क्या स्केल में संग्रह पर तत्व वार ऑपरेशन करने का कोई और तेज़ तरीका है?

दुखद सच्चाई यह है कि यह निर्णायक होने के बावजूद, उत्पादकता में सुधार, और बग के लिए लचीलापन है कि कार्यात्मक भाषाएं आवश्यक रूप से सबसे अधिक प्रदर्शनकारी नहीं हैं - संग्रह को मुक्त नहीं करने के खिलाफ एक प्रक्षेपण को परिभाषित करने के लिए उच्च आदेश कार्यों का उपयोग करना, और आपका तंग लूप इस पर प्रकाश डालता है। जैसा कि अन्य ने बताया है, मध्यवर्ती और अंतिम परिणामों के लिए अतिरिक्त भंडारण आवंटन भी ओवरहेड होंगे।

यदि प्रदर्शन महत्वपूर्ण है, हालांकि किसी भी तरह से सार्वभौमिक नहीं है, तो आपके जैसे मामलों में आप स्केला के संचालन को वापस कर सकते हैं ताकि स्मृति उपयोग पर अधिक प्रत्यक्ष नियंत्रण हासिल करने और फ़ंक्शन कॉल को समाप्त करने के लिए अनिवार्य समकक्षों में वापस आ सकें।

अपने विशिष्ट उदाहरण में, zipped रकमों को पूर्व-निश्चित, सही आकार के परिवर्तनशील सरणी को आवंटित करके अनिवार्य रूप से प्रदर्शन किया जा सकता है (क्योंकि ज़िप में से एक जब तत्वों में से एक से बाहर निकलता है), और फिर एक साथ उपयुक्त सूचकांक में तत्वों को जोड़ना (एक्सेस करने के बाद से) आर्डिनल इंडेक्स द्वारा एरे तत्व एक बहुत तेज संचालन है)।

तीसरा फ़ंक्शन जोड़ना, ES3अपने परीक्षण सूट में :

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
}

मेरे i7 पर मुझे निम्न प्रतिक्रिया समय मिलता है:

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

इससे भी अधिक जघन्य यह होगा कि दो सरणियों में से छोटे का सीधा-सीधा उत्परिवर्तन किया जाएगा, जो स्पष्ट रूप से किसी एक सारणी की सामग्री को भ्रष्ट करेगा, और केवल तभी किया जाएगा जब मूल सरणी की दोबारा आवश्यकता नहीं होगी:

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

लेकिन जाहिर है, सरणी तत्वों का प्रत्यक्ष उत्परिवर्तन स्काला की भावना में नहीं है।


2
ऊपर मेरे कोड में कुछ भी समानांतर नहीं है। यद्यपि यह विशिष्ट समस्या समांतर है (क्योंकि कई थ्रेड सरणियों के विभिन्न वर्गों पर काम कर सकते हैं), केवल 10k तत्वों पर इस तरह के एक सरल ऑपरेशन में बहुत अधिक बिंदु नहीं होंगे - नए धागे बनाने और सिंक्रनाइज़ करने का ओवरहेड संभवतः किसी भी लाभ से आगे निकल जाएगा। । ईमानदार होने के लिए, यदि आपको प्रदर्शन अनुकूलन के इस स्तर की आवश्यकता है, तो आप इन प्रकार के एल्गोरिदम को रस्ट, गो या सी में लिखना बेहतर
समझ सकते हैं

3
यह Array.tabulate(minSize)(i => arr(i) + arr1(i))आपकी सरणी बनाने के लिए उपयोग करने के लिए अधिक स्केला जैसा और तेज़ होगा
सर्वेश कुमार सिंह

1
@ सर्वेशकुमारसिंह यह बहुत धीमा है। लगभग 9 सेकंड लेता है
user12140540

1
Array.tabulatezipया तो zippedयहाँ से बहुत तेज़ होना चाहिए (और मेरे बेंचमार्क में है)।
ट्रैविस ब्राउन

1
@ स्टुअर्टएलसी "प्रदर्शन केवल तभी समतुल्य होगा जब उच्च क्रम फ़ंक्शन किसी तरह से अपरिवर्तित और इनबिल्ट हो।" यह वास्तव में सही नहीं है। यहां तक ​​कि आपका forएक उच्च-क्रम फ़ंक्शन कॉल ( foreach) के लिए desugared है । लंबोदर को दोनों मामलों में केवल एक बार त्वरित किया जाएगा।
ट्रैविस ब्राउन

50

अन्य उत्तरों में से कोई भी गति में अंतर के प्राथमिक कारण का उल्लेख नहीं करता है, जो यह है कि zippedसंस्करण 10,000 टपल आवंटन से बचा जाता है। अन्य जवाबों के एक जोड़े के रूप में नोट करते हैं , zipसंस्करण में एक मध्यवर्ती सरणी शामिल है, जबकि zippedसंस्करण नहीं है, लेकिन 10,000 तत्वों के लिए एक सरणी आवंटित करना वह नहीं है जो zipसंस्करण को इतना बदतर बना देता है - यह 10,000 अल्पकालिक ट्यूपल है जो उस सरणी में डाला जा रहा है। ये JVM पर वस्तुओं द्वारा दर्शाए जाते हैं, इसलिए आप उन चीजों के लिए ऑब्जेक्ट आवंटन का एक गुच्छा बना रहे हैं, जिन्हें आप तुरंत फेंकने जा रहे हैं।

इस उत्तर के बाकी हिस्से के बारे में थोड़ा और विस्तार से जाना जाता है कि आप इसकी पुष्टि कैसे कर सकते हैं।

बेहतर बेंचमार्किंग

आप वास्तव में JVM पर जिम्मेदारी से किसी भी प्रकार की बेंचमार्किंग करने के लिए jmh जैसे फ्रेमवर्क का उपयोग करना चाहते हैं , और फिर भी जिम्मेदारी वाला हिस्सा कठिन है, हालाँकि 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अभी भी स्कैला संग्रह के संचालन में वांछित होगा। हम इस कार्यान्वयन की तुलना कर सकते हैं ( 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 संस्करणों अधिक पठनीय हैं, मैं व्यक्तिगत रूप से कोई मतलब है, जिसमें जरूरी संस्करणों "स्काला की भावना" के खिलाफ हैं वहाँ नहीं लगता कि, और मैं संकोच नहीं होगा उन्हें खुद का उपयोग करने के लिए।

सारणी के साथ

अद्यतन: मैंने एक 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

यह वही है, जिसकी मुझे उम्मीद है, क्योंकि किसी फ़ंक्शन को कॉल करने के बारे में कुछ भी महंगा नहीं है, और क्योंकि सूचकांक द्वारा सरणी तत्वों तक पहुंचना बहुत सस्ता है।


8

विचार करें lazyZip

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

के बजाय zip

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

स्कोर 2.13 के पक्ष में जोड़ा lazyZip गया.zipped

.zipविचारों पर एक साथ , यह जगह .zipped(अब पदावनत)। ( स्काला / संग्रह-स्ट्रॉमन # 223 )

zippedटिम और माइक एलेन द्वारा समझाए गए (और इसलिए lazyZip) की तुलना में तेज़ है , इसके बाद सख्ती के कारण दो अलग-अलग परिवर्तन होंगे, जबकिzipzipmapzipped इसके बाद से mapआलस्य के कारण एक ही बार में मार डाला एक भी परिवर्तन का परिणाम देगा।

zippedदेता है Tuple2Zipped, और विश्लेषण 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()
  }

हम दो संग्रह देखने coll1और coll2अधिक दोहराया गया है और प्रत्येक यात्रा पर समारोह fके लिए पारित mapरास्ते लागू किया जाता है

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

मध्यस्थ संरचनाओं को आवंटित करने और बदलने के बिना।


लागू करने ट्रैविस 'विधि बेंचमार्किंग, यहाँ नया बीच तुलना है lazyZipऔर पदावनत zippedजहां

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

देता है

[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

lazyZipथोड़ा की तुलना में बेहतर प्रदर्शन करने के लिए लगता है zippedपर ArraySeq। दिलचस्प बात यह है का उपयोग करते समय काफी अवक्रमित प्रदर्शन नोटिस lazyZipपर Array


lazzZip Scala 2.13.1 में उपलब्ध है। वर्तमान में मैं Scala 2.11.8
user12140540

5

जेआईटी संकलन के कारण आपको प्रदर्शन माप से हमेशा सतर्क रहना चाहिए, लेकिन एक संभावित कारण यह है कि zippedआलसी और कॉल के Arrayदौरान मूल वाहिकाओं से तत्व निकालता है map, जबकि zipएक नई Arrayवस्तु बनाता है और फिर mapनई वस्तु पर कॉल करता है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.