पुनरावर्ती विधि कॉल कारण StackOverFlowError कोटलिन में लेकिन जावा में नहीं


14

जावा और कोटलिन में मेरे दो समान कोड हैं

जावा:

public void reverseString(char[] s) {
    helper(s, 0, s.length - 1);
}

public void helper(char[] s, int left, int right) {
    if (left >= right) return;
    char tmp = s[left];
    s[left++] = s[right];
    s[right--] = tmp;
    helper(s, left, right);
}

Kotlin:

fun reverseString(s: CharArray): Unit {
    helper(0, s.lastIndex, s)
}

fun helper(i: Int, j: Int, s: CharArray) {
    if (i >= j) {
        return
    }
    val t = s[j]
    s[j] = s[i]
    s[i] = t
    helper(i + 1, j - 1, s)
}

जावा कोड एक विशाल इनपुट के साथ परीक्षा उत्तीर्ण करता है, लेकिन कोटलिन कोड फ़ंक्शन का कारण बनता है StackOverFlowErrorजब तक कि मैं कोटलिन में फ़ंक्शन tailrecसे पहले कीवर्ड नहीं helperजोड़ता।

मैं जानना चाहता हूं कि यह फंक्शन जावा में काम करता है और कोलिन में भी, tailrecलेकिन बिना कोटलिन में नहीं tailrec?

पुनश्च: मुझे पता है क्या tailrecकरते हैं


1
जब मैंने इनका परीक्षण किया, तो मैंने पाया कि जावा संस्करण सरणी आकारों के लिए लगभग 29500 तक काम करेगा, लेकिन कोटलिन संस्करण 18500 के आसपास बंद हो जाएगा। यह एक महत्वपूर्ण अंतर है, लेकिन बहुत बड़ा नहीं है। यदि आपको बड़े सरणियों के लिए काम करने के लिए इसकी आवश्यकता है, तो tailrecपुनरावृत्ति से बचने या उपयोग करने का एकमात्र अच्छा समाधान है ; उपलब्ध स्टैक आकार JVMs और सेट-अप के बीच, और विधि और उसके परिमाण के बीच, रन के बीच भिन्न होता है। लेकिन अगर आप शुद्ध जिज्ञासा (पूरी तरह से अच्छा कारण!) से बाहर पूछ रहे हैं, तो मुझे यकीन नहीं है। आप शायद bytecode को देखने की आवश्यकता होगी।
gidds

जवाबों:


7

मुझे पता है कि क्यों इस समारोह में काम करता है चाहते हैं जावा में है और यह भी kotlin साथ tailrecलेकिन में नहीं kotlin बिना tailrec?

संक्षिप्त उत्तर इसलिए है क्योंकि आपकी कोटलिन विधि JAVA की तुलना में "भारी" है । हर कॉल पर यह एक और तरीका कहता है जो "उत्तेजित करता है" StackOverflowError। तो, नीचे एक अधिक विस्तृत विवरण देखें।

के लिए जावा बाइटकोड समतुल्य reverseString()

मैंने कोटलिन और जावा में आपके तरीकों के लिए बाइट कोड की जाँच की :

JAVA में कोटलिन विधि bytecode

...
public final void reverseString(@NotNull char[] s) {
    Intrinsics.checkParameterIsNotNull(s, "s");
    this.helper(0, ArraysKt.getLastIndex(s), s);
}

public final void helper(int i, int j, @NotNull char[] s) {
    Intrinsics.checkParameterIsNotNull(s, "s");
    if (i < j) {
        char t = s[j];
        s[j] = s[i];
        s[i] = t;
        this.helper(i + 1, j - 1, s);
    }
}
...

जावा में JAVA विधि बायोटेक

...
public void reverseString(char[] s) {
    this.helper(s, 0, s.length - 1);
}

public void helper(char[] s, int left, int right) {
    if (left < right) {
        char temp = s[left];
        s[left++] = s[right];
        s[right--] = temp;
        this.helper(left, right, s);
    }
}
...

तो, वहाँ 2 मुख्य अंतर हैं:

  1. Intrinsics.checkParameterIsNotNull(s, "s")कोटलिन संस्करण helper()में प्रत्येक के लिए आमंत्रित किया गया है ।
  2. जेएवीए विधि में बाएं और दाएं सूचकांकों में वृद्धि होती है, जबकि कोटलिन में प्रत्येक पुनरावर्ती कॉल के लिए नए सूचकांक बनाए जाते हैं।

तो, आइए परीक्षण करें कि Intrinsics.checkParameterIsNotNull(s, "s")अकेले व्यवहार को कैसे प्रभावित करता है।

दोनों कार्यान्वयन का परीक्षण करें

मैंने दोनों मामलों के लिए एक साधारण परीक्षण बनाया है:

@Test
public void testJavaImplementation() {
    char[] chars = new char[20000];
    new Example().reverseString(chars);
}

तथा

@Test
fun testKotlinImplementation() {
    val chars = CharArray(20000)
    Example().reverseString(chars)
}

के लिए जावा परीक्षण समस्याओं के बिना सफल रहा थोड़ी देर के लिए Kotlin यह एक की वजह से बुरी तरह विफल रही है StackOverflowError। हालांकि, के बाद मैं जोड़ा Intrinsics.checkParameterIsNotNull(s, "s")करने के लिए जावा यह रूप में अच्छी तरह विफल रहा है विधि:

public void helper(char[] s, int left, int right) {
    Intrinsics.checkParameterIsNotNull(s, "s"); // add the same call here

    if (left >= right) return;
    char tmp = s[left];
    s[left] = s[right];
    s[right] = tmp;
    helper(s, left + 1, right - 1);
}

निष्कर्ष

आपकी कोटलिन पद्धति में एक छोटी पुनरावृत्ति गहराई है क्योंकि यह Intrinsics.checkParameterIsNotNull(s, "s")हर कदम पर आह्वान करती है और इस तरह यह अपने जावा समकक्ष की तुलना में भारी है । यदि आप इस स्वतः-जनरेटेड विधि को नहीं चाहते हैं, तो आप संकलन के दौरान अशक्त जाँच को अक्षम कर सकते हैं जैसा कि यहाँ पर उत्तर दिया गया है

हालाँकि, जब से आप समझते हैं कि क्या लाभ tailrecलाता है (अपने पुनरावर्ती कॉल को पुनरावृत्ति में परिवर्तित करता है) आपको उस एक का उपयोग करना चाहिए।


@ user207421 हर विधि आह्वान का अपना स्टैक फ्रेम शामिल है Intrinsics.checkParameterIsNotNull(...)। जाहिर है, इस तरह के प्रत्येक स्टैक फ्रेम को निश्चित मात्रा में मेमोरी की आवश्यकता होती है (के लिए LocalVariableTableऔर ऑपरेंड स्टैक और इतने पर) ..
अनातोली

0

कोटलिन सिर्फ एक छोटे से अधिक स्टैक भूखा है (Int object params io int params)। टेलरेक सॉल्यूशन के अलावा जो यहां फिट होता है, आप स्थानीय वेरिएबल को एक्सोर-इंग tempद्वारा खत्म कर सकते हैं:

fun helper(i: Int, j: Int, s: CharArray) {
    if (i >= j) {
        return
    }               // i: a          j: b
    s[j] ^= s[i]    //               j: a^b
    s[i] ^= s[j]    // i: a^a^b == b
    s[j] ^= s[i]    //               j: a^b^b == a
    helper(i + 1, j - 1, s)
}

पूरी तरह से निश्चित नहीं है कि यह एक स्थानीय चर को हटाने के लिए काम करता है या नहीं।

जे को खत्म करने से भी हो सकता है:

fun reverseString(s: CharArray): Unit {
    helper(0, s)
}

fun helper(i: Int, s: CharArray) {
    if (i >= s.lastIndex - i) {
        return
    }
    val t = s[s.lastIndex - i]
    s[s.lastIndex - i] = s[i]
    s[i] = t
    helper(i + 1, s)
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.