कैसे स्काला में समझ और छोरों का अनुकूलन करने के लिए?


131

तो स्काला को जावा के समान तेज़ माना जाता है। मैं स्कैला में कुछ प्रोजेक्ट यूलर समस्याओं पर फिर से विचार कर रहा हूं जो मैंने मूल रूप से जावा में निपटाए थे। विशेष रूप से समस्या 5: "सबसे छोटी सकारात्मक संख्या क्या है जो 1 से 20 तक की सभी संख्याओं से समान रूप से विभाज्य है?"

यहां मेरा जावा समाधान है, जिसे मेरी मशीन पर पूरा करने में 0.7 सेकंड लगते हैं:

public class P005_evenly_divisible implements Runnable{
    final int t = 20;

    public void run() {
        int i = 10;
        while(!isEvenlyDivisible(i, t)){
            i += 2;
        }
        System.out.println(i);
    }

    boolean isEvenlyDivisible(int a, int b){
        for (int i = 2; i <= b; i++) {
            if (a % i != 0) 
                return false;
        }
        return true;
    }

    public static void main(String[] args) {
        new P005_evenly_divisible().run();
    }
}

यहाँ मेरा "प्रत्यक्ष अनुवाद" स्काला में है, जिसमें 103 सेकंड (147 गुना अधिक समय लगता है!)

object P005_JavaStyle {
    val t:Int = 20;
    def run {
        var i = 10
        while(!isEvenlyDivisible(i,t))
            i += 2
        println(i)
    }
    def isEvenlyDivisible(a:Int, b:Int):Boolean = {
        for (i <- 2 to b)
            if (a % i != 0)
                return false
        return true
    }
    def main(args : Array[String]) {
        run
    }
}

अंत में यहां कार्यात्मक प्रोग्रामिंग पर मेरा प्रयास है, जिसमें 39 सेकंड (55 गुना अधिक समय) लगता है

object P005 extends App{
    def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}
    def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
    println (find (2))
}

विंडोज 7 64-बिट पर स्काला 2.9.0.1 का उपयोग करना। मैं प्रदर्शन कैसे सुधारूं? क्या मुझसे कुछ गलत हो रही है? या जावा बहुत तेज है?


2
क्या आप स्केला शेल का उपयोग कर संकलन या व्याख्या करते हैं?
अहमतबी -

परीक्षण प्रभाग ( संकेत ) का उपयोग करने की तुलना में ऐसा करने का एक बेहतर तरीका है ।
हमर

2
आप यह नहीं दिखा रहे हैं कि आप यह कैसे कर रहे हैं। क्या आपने केवल runविधि का समय निकालने की कोशिश की ?
आरोन नोवस्त्रुप

2
@hammar - हाँ, बस इसे कलम और कागजी तरीके से किया था: उच्च के साथ शुरू होने वाले प्रत्येक नंबर के लिए मुख्य कारक लिखें, फिर उन कारकों को पार करें जो आपके पास पहले से ही उच्च संख्या के लिए हैं, इसलिए आप (5 * 2 * 2) के साथ समाप्त करें * (१ ९) * (३ * ३) * (१ () * (२ * २) * () * (*) * (१३) * () * (११) = २३२25
लुइगी प्लिंज

2
+1 यह सबसे दिलचस्प सवाल है जो मैंने एसओ पर हफ्तों में देखा है (यह भी सबसे अच्छा जवाब है जो मैंने काफी समय में देखा है)।
मिया क्लार्क

जवाबों:


111

इस विशेष मामले में समस्या यह है कि आप फॉर-एक्सप्रेशन के भीतर से लौटते हैं। बदले में इसका अनुवाद एक NonLocalReturnException के थ्रो में हो जाता है, जिसे एन्कोडिंग विधि में पकड़ा जाता है। ऑप्टिमाइज़र फ़ॉर्चेक को समाप्त कर सकता है लेकिन अभी तक थ्रो / कैच को समाप्त नहीं कर सकता है। और फेंकना / पकड़ना महंगा है। लेकिन चूंकि इस तरह के नेस्टेड रिटर्न स्काला कार्यक्रमों में दुर्लभ हैं, इसलिए आशावादी ने इस मामले को अभी तक संबोधित नहीं किया है। आशावादी को बेहतर बनाने के लिए काम चल रहा है जो उम्मीद करता है कि जल्द ही इस मुद्दे को हल कर दिया जाएगा।


9
बहुत भारी है कि एक वापसी एक अपवाद बन जाता है। मुझे यकीन है कि यह कहीं न कहीं प्रलेखित है, लेकिन इसमें बिना छिपे हुए जादू की पुनरावृत्ति है। क्या वास्तव में यही एकमात्र रास्ता है?
स्क्रेबेल

10
यदि रिटर्न किसी क्लोजर के अंदर से होता है, तो यह सबसे अच्छा उपलब्ध विकल्प लगता है। बाहर के क्लोजर से रिटर्न निश्चित रूप से बाईटेकोड में निर्देशों को वापस करने के लिए सीधे अनुवाद किया जाता है।
मार्टिन ओडस्की

1
मुझे यकीन है कि मैं कुछ अनदेखी कर रहा हूं, लेकिन इसके बजाय एक बंद बूलियन ध्वज और रिटर्न-वैल्यू सेट करने के लिए एक क्लोजर के अंदर से रिटर्न को संकलित क्यों नहीं करता है, और जांच करें कि क्लोजर-कॉल रिटर्न के बाद?
ल्यूक हट्टमैन

9
उसका कार्यात्मक एल्गोरिथ्म अभी भी 55 गुना धीमा क्यों है? ऐसा नहीं लगता है कि इस तरह के भयानक प्रदर्शन से पीड़ित होना चाहिए
एलिय्याह

4
अब, 2014 में, मैंने इसे फिर से परीक्षण किया और मेरे लिए प्रदर्शन निम्नलिखित है: जावा -> 0.3; scala -> 3.6; scala अनुकूलित -> 3.5 s; स्कैला कार्यात्मक -> 4 जी; 3 साल पहले की तुलना में बहुत अच्छा लग रहा है, लेकिन ... फिर भी अंतर बहुत बड़ा है। क्या हम अधिक प्रदर्शन सुधार की उम्मीद कर सकते हैं? दूसरे शब्दों में, मार्टिन, क्या कुछ भी है, सिद्धांत में, संभावित अनुकूलन के लिए छोड़ दिया गया है?
साशा.सोचका

80

समस्या सबसे अधिक संभावना forहै कि विधि में एक समझ का उपयोग isEvenlyDivisibleforएक बराबर whileलूप द्वारा प्रतिस्थापित करने से जावा के साथ प्रदर्शन अंतर को समाप्त करना चाहिए।

जैसा कि जावा के forछोरों के विपरीत है , स्काला की forसमझ वास्तव में उच्च-क्रम विधियों के लिए सिंटैक्टिक चीनी है; इस मामले में, आप foreachकिसी Rangeऑब्जेक्ट पर विधि को कॉल कर रहे हैं । स्काला forबहुत सामान्य है, लेकिन कभी-कभी दर्दनाक प्रदर्शन होता है।

आप -optimizeस्काला संस्करण 2.9 में ध्वज को आज़माना चाहते हैं । देखे गए प्रदर्शन विशेष JVM के उपयोग पर निर्भर हो सकते हैं, और JIT आशावादी के पास हॉट-स्पॉट की पहचान और अनुकूलन के लिए पर्याप्त "वार्म अप" समय है।

मेलिंग सूची पर हालिया चर्चाओं से संकेत मिलता है कि स्काला टीम forसरल मामलों में प्रदर्शन में सुधार लाने पर काम कर रही है :

यहाँ बग ट्रैकर में मुद्दा है: https://issues.scala-lang.org/browse/SI-4633

अद्यतन 5/28 :

  • एक अल्पावधि समाधान के रूप में, स्कालाक्लियर प्लगइन (अल्फा) सरल स्काला छोरों को छोरों के बराबर में बदल देगा while
  • संभावित दीर्घकालिक समाधान के रूप में, ईपीएफएल और स्टैनफोर्ड की टीमें बहुत ही उच्च प्रदर्शन के लिए "आभासी" स्काला के रन-टाइम संकलन को सक्षम करने वाली परियोजना पर सहयोग कर रही हैं । उदाहरण के लिए, कई आइडियल फंक्शनल लूप्स को रन-टाइम पर इष्टतम JVM बाइटेकोड में या किसी अन्य लक्ष्य जैसे कि GPU के रूप में इस्तेमाल किया जा सकता है। प्रणाली एक्स्टेंसिबल है, जिससे उपयोगकर्ता परिभाषित डीएसएल और ट्रांसफॉर्मेशन की अनुमति देता है। प्रकाशनों और स्टैनफोर्ड पाठ्यक्रम नोटों की जाँच करें । प्रीथिनरी कोड जीथब पर उपलब्ध है, आने वाले महीनों में रिलीज़ होने का इरादा है।

6
महान, मैंने थोड़ी देर के लूप के साथ समझ की जगह ले ली और यह जावा संस्करण के समान गति (+/- <1%) चलाता है। धन्यवाद ... मैं लगभग एक मिनट के लिए स्काला में विश्वास खो दिया! अब बस एक अच्छे कार्यात्मक एल्गोरिथ्म पर काम करना होगा ... :)
लुइगी प्लिंज

24
यह ध्यान देने योग्य है कि पूंछ-पुनरावर्ती कार्य भी उतनी ही तेजी से होते हैं, जबकि लूप (चूंकि दोनों बहुत समान या समान बायोटेक में परिवर्तित होते हैं)।
रेक्स केर

7
यह मुझे एक बार भी मिल गया। अविश्वसनीय धीमी-डाउन के कारण लूप्स (स्तर 6!) में संग्रह कार्यों का उपयोग करने से एल्गोरिथ्म का अनुवाद करना था। यह एक ऐसी चीज है जिसे बहुत अधिक लक्षित करने की आवश्यकता है, इमो; यदि मुझे सभ्य (आवश्यकता: तेज धधकते नहीं) प्रदर्शन की आवश्यकता हो तो मैं इसका उपयोग कैसे करूँ, यह एक अच्छी प्रोग्रामिंग शैली है।
राफेल

7
कब forउपयुक्त है?
OscarRyz

@OscarRyz - एक के लिए scala व्यवहार करता है (:) जावा में, अधिकांश भाग के लिए।
माइक ऐजक

31

फॉलो-अप के रूप में, मैंने -ॉप्टिमाइज़ फ्लैग की कोशिश की और यह 103 से 76 सेकंड तक चलने का समय कम कर दिया, लेकिन यह जावा या थोड़ी देर के लूप की तुलना में अभी भी 107x धीमा है।

तब मैं "कार्यात्मक" संस्करण देख रहा था:

object P005 extends App{
  def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}
  def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
  println (find (2))
}

और यह पता लगाने की कोशिश कर रहा है कि संक्षिप्त रूप में "forall" से कैसे छुटकारा पाया जाए। मैं बुरी तरह विफल रहा और साथ आया

object P005_V2 extends App {
  def isDivis(x:Int):Boolean = {
    var i = 1
    while(i <= 20) {
      if (x % i != 0) return false
      i += 1
    }
    return true
  }
  def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
  println (find (2))
}

जिससे मेरी चालाक 5-लाइन समाधान 12 लाइनों के लिए तैयार हो गया है। हालाँकि, यह संस्करण 0.71 सेकंड में चलता है , मूल जावा संस्करण की तरह ही गति है, और "forall" (40.2 s) का उपयोग करके संस्करण की तुलना में 56 गुना तेज है! (यह जावा की तुलना में तेज़ क्यों है, इसके लिए नीचे EDIT देखें)

जाहिर है मेरा अगला कदम ऊपर के जावा में अनुवाद करना था, लेकिन जावा इसे संभाल नहीं सकता है और 22000 के आसपास n के साथ एक StackOverflowError फेंकता है।

मैंने फिर अपने सिर को थोड़ा खरोंच दिया और "जबकि" को थोड़ा और पूंछ की पुनरावृत्ति के साथ बदल दिया, जो कुछ पंक्तियों को बचाता है, बस उतना ही तेज चलता है, लेकिन चलो इसका सामना करते हैं, पढ़ने के लिए और अधिक भ्रामक है:

object P005_V3 extends App {
  def isDivis(x:Int, i:Int):Boolean = 
    if(i > 20) true
    else if(x % i != 0) false
    else isDivis(x, i+1)

  def find(n:Int):Int = if (isDivis(n, 2)) n else find (n+2)
  println (find (2))
}

इसलिए स्काला की पूंछ पुनरावृत्ति दिन जीतती है, लेकिन मुझे आश्चर्य है कि "पाश" (और "फोर्ल" विधि) के रूप में कुछ सरल रूप से टूट गया है और इसे अप्राकृतिक और वर्बोज़ "व्हाइल्स", या पूंछ पुनरावृत्ति द्वारा प्रतिस्थापित किया जाना है । बहुत से कारण मैं स्कैला की कोशिश कर रहा हूं क्योंकि संक्षिप्त सिंटैक्स है, लेकिन यह अच्छा नहीं है अगर मेरा कोड 100% धीमा चलाने वाला है!

संपादित करें : (हटाया गया)

EDIT OF EDIT : 2.5 और 0.7 के रन समय के बीच पूर्व की विसंगतियां पूरी तरह से 32-बिट या 64-बिट जेवीएम का उपयोग किए जाने के कारण थीं। कमांड लाइन से स्केला JAVA_HOME द्वारा जो कुछ भी सेट किया गया है, उसका उपयोग करता है, जबकि जावा 64-बिट का उपयोग करता है यदि उपलब्ध नहीं है। आईडीई की अपनी सेटिंग होती है। यहाँ कुछ माप: ग्रहण में स्कला निष्पादन समय


1
isDivis-विधि के रूप में लिखा जा सकता है: def isDivis(x: Int, i: Int): Boolean = if (i > 20) true else if (x % i != 0) false else isDivis(x, i+1)। ध्यान दें कि स्काला में if-else एक अभिव्यक्ति है जो हमेशा एक मान लौटाता है। यहां रिटर्न-कीवर्ड की जरूरत नहीं है।
किरित्सुकु

3
आपके अंतिम संस्करण ( P005_V3) को छोटा, अधिक घोषणापत्र और IMHO स्पष्ट करके लिखा जा सकता है:def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
Blaisorblade

@ ब्लेज़रब्लड नं। यह पूंछ-पुनरावृत्ति को तोड़ देगा, जिसे बायटेकोड में थोड़ी देर के लूप में अनुवाद करना आवश्यक है, जो बदले में निष्पादन को तेज करता है।
gzm0

4
मैं आपकी बात देख रहा हूं, लेकिन मेरा उदाहरण अभी तक && और || शॉर्ट-सर्किट मूल्यांकन का उपयोग करें, जैसा कि @atelrec: gist.github.com/Blaisorblade/5672562
Blaisorblade

8

समझ के लिए जवाब सही है, लेकिन यह पूरी कहानी नहीं है। आप टिप्पणी नोट करना चाहिए कि के उपयोग returnमें isEvenlyDivisibleमुक्त नहीं है। के अंदर वापसी का उपयोग for, scala संकलक को एक गैर-स्थानीय रिटर्न (यानी यह फ़ंक्शन के बाहर लौटने के लिए) उत्पन्न करने के लिए मजबूर करता है।

यह लूप से बाहर निकलने के लिए एक अपवाद के उपयोग के माध्यम से किया जाता है। उदाहरण के लिए, यदि आप अपने नियंत्रण सार का निर्माण करते हैं, तो ऐसा ही होता है:

def loop[T](times: Int, default: T)(body: ()=>T) : T = {
    var count = 0
    var result: T = default
    while(count < times) {
        result = body()
        count += 1
    }
    result
}

def foo() : Int= {
    loop(5, 0) {
        println("Hi")
        return 5
    }
}

foo()

यह केवल एक बार "हाय" प्रिंट करता है।

ध्यान दें कि returnमें fooबाहर निकलता है foo(जो आप क्या उम्मीद करेंगे है)। चूंकि ब्रैकेटेड अभिव्यक्ति एक फ़ंक्शन शाब्दिक है, जिसे आप इस हस्ताक्षर में देख सकते हैं loopकि कंपाइलर एक गैर स्थानीय रिटर्न उत्पन्न करने के लिए मजबूर करता है, returnअर्थात् foo, आपको बाहर निकलने के लिए मजबूर करता है , न कि केवल body

जावा (यानी जेवीएम) में इस तरह के व्यवहार को लागू करने का एकमात्र तरीका अपवाद फेंकना है।

वापस जा रहे हैं isEvenlyDivisible:

def isEvenlyDivisible(a:Int, b:Int):Boolean = {
  for (i <- 2 to b) 
    if (a % i != 0) return false
  return true
}

if (a % i != 0) return falseएक समारोह के शाब्दिक एक वापसी है कि, हर बार वापसी मारा जाता है तो, क्रम फेंक और एक अपवाद है, जो भूमि के ऊपर काफी जीसी का एक सा कारण बनता है को पकड़ने के लिए है।


6

तेजी लाने के कुछ तरीके forallमेरे द्वारा खोजे गए :

मूल: 41.3 एस

def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}

रेंज का पूर्व-त्वरितकरण, इसलिए हम हर बार एक नई सीमा नहीं बनाते हैं: 9.0 एस

val r = (1 to 20)
def isDivis(x:Int) = r forall {x % _ == 0}

सीमा के बजाय एक सूची में बदलना: 4.8 s

val rl = (1 to 20).toList
def isDivis(x:Int) = rl forall {x % _ == 0}

मैंने कुछ अन्य संग्रहों की कोशिश की, लेकिन सूची सबसे तेज थी (हालांकि अभी भी 7x धीमी है अगर हम रेंज और उच्च-क्रम फ़ंक्शन से पूरी तरह से बचते हैं)।

जब मैं स्काला के लिए नया हूं, तो मुझे लगता है कि कंपाइलर आसानी से एक त्वरित और महत्वपूर्ण प्रदर्शन लाभ को लागू कर सकता है, बस बाहरी तौर पर रेंज स्थिरांक के साथ तरीकों में रेंज शाब्दिकों को स्वचालित रूप से प्रतिस्थापित कर सकता है। या बेहतर, उन्हें जावा में स्ट्रिंग्स शाब्दिक की तरह इंटर्न करें


फुटनोट : एरेस रेंज के समान ही थे, लेकिन दिलचस्प बात यह है कि एक नई forallविधि को पिंपल करने (नीचे दिखाया गया) के परिणामस्वरूप 64-बिट पर 24% तेजी से निष्पादन हुआ, और 32-बिट पर 8% तेजी से। जब मैंने 20 से 15 के अंतर से कारकों की संख्या को कम करके गणना आकार को घटा दिया, तो शायद यह कचरा संग्रहण प्रभाव है। जो भी कारण हो, विस्तारित अवधि के लिए पूर्ण लोड के तहत काम करते समय यह महत्वपूर्ण है।

सूची के लिए इसी तरह के एक दलाल ने भी लगभग 10% बेहतर प्रदर्शन किया।

  val ra = (1 to 20).toArray
  def isDivis(x:Int) = ra forall2 {x % _ == 0}

  case class PimpedSeq[A](s: IndexedSeq[A]) {
    def forall2 (p: A => Boolean): Boolean = {      
      var i = 0
      while (i < s.length) {
        if (!p(s(i))) return false
        i += 1
      }
      true
    }    
  }  
  implicit def arrayToPimpedSeq[A](in: Array[A]): PimpedSeq[A] = PimpedSeq(in)  

3

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

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


2

स्काला के लिए विशिष्ट समस्याओं पर पहले ही चर्चा की जा चुकी है, लेकिन मुख्य समस्या यह है कि एक जानवर-बल एल्गोरिथ्म का उपयोग करना बहुत अच्छा नहीं है। इस पर विचार करें (मूल जावा कोड से बहुत तेज):

def gcd(a: Int, b: Int): Int = {
    if (a == 0)
        b
    else
        gcd(b % a, a)
}
print (1 to 20 reduce ((a, b) => {
  a / gcd(a, b) * b
}))

सवाल भाषाओं में एक विशिष्ट तर्क के प्रदर्शन की तुलना करते हैं। क्या एल्गोरिथ्म समस्या के लिए इष्टतम है, क्या सारहीन है।
smartnut007

1

प्रोजेक्ट यूलर के लिए स्कैला में दिए गए वन-लाइनर को आज़माएं

दिया गया समय आपके मुकाबले कम से कम तेज़ है, हालाँकि लूप से बहुत दूर है .. :)


यह मेरे कार्यात्मक संस्करण के समान है। आप खान के रूप में लिख सकते हैं def r(n:Int):Int = if ((1 to 20) forall {n % _ == 0}) n else r (n+2); r(2), जो कि पावेल की तुलना में 4 अक्षर छोटा है। :) हालाँकि मैं अपने कोड का ढोंग नहीं करता, लेकिन जब मैंने इस प्रश्न को पोस्ट किया तो मैंने लगभग 30 पंक्तियों की स्कैला को कोड कर दिया था।
लुइगी प्लिंज
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.