การเรียกเมธอดแบบเรียกซ้ำทำให้เกิด StackOverFlowError ใน kotlin แต่ไม่ใช่ใน java


14

ฉันมีสองรหัสที่เหมือนกันเกือบทั้งใน java และ kotlin

Java:

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

โค้ด java ผ่านการทดสอบด้วยอินพุตจำนวนมาก แต่โค้ด kotlin ทำให้ a StackOverFlowErrorยกเว้นว่าฉันได้เพิ่มtailrecคีย์เวิร์ดไว้ก่อนหน้าhelperฟังก์ชันใน kotlin

ฉันต้องการที่จะรู้ว่าทำไมฟังก์ชั่นนี้ใช้งานได้ในจาวาและยังอยู่ในโคลินด้วยtailrecแต่ไม่ได้อยู่ใน kotlin โดยไม่ต้องtailrec?

PS: ฉันรู้ว่าtailrecทำอะไร


1
เมื่อฉันทดสอบสิ่งเหล่านี้ฉันพบว่าเวอร์ชัน Java จะใช้งานได้กับขนาดอาร์เรย์สูงถึงประมาณ 29500 แต่รุ่น Kotlin จะหยุดประมาณ 18500 นั่นเป็นความแตกต่างที่สำคัญ แต่ไม่ใช่ขนาดที่ใหญ่มาก หากคุณต้องการให้มันทำงานกับอาร์เรย์ขนาดใหญ่ทางออกที่ดีเพียงทางเดียวคือการใช้tailrecหรือหลีกเลี่ยงการเรียกซ้ำ ขนาดสแต็กที่มีให้จะแตกต่างกันระหว่างการวิ่งระหว่าง JVM และการตั้งค่าและขึ้นอยู่กับวิธีการและพารามิเตอร์ แต่ถ้าคุณถามถึงความอยากรู้อย่างแท้จริง (เป็นเหตุผลที่ดีอย่างสมบูรณ์แบบ!) ฉันก็ไม่แน่ใจ คุณอาจต้องดูที่ bytecode
gidds

คำตอบ:


7

ฉันต้องการทราบว่าทำไมฟังก์ชั่นนี้ใช้งานได้ในjavaและในkotlinด้วยtailrecแต่ไม่ใช่kotlinหากไม่มีtailrec?

คำตอบสั้น ๆ เป็นเพราะคุณKotlinวิธีคือ "หนัก" กว่าJAVAหนึ่ง ในทุกสายที่เรียกว่าวิธีการอื่นที่ StackOverflowError"กระตุ้นความ" ดังนั้นดูคำอธิบายโดยละเอียดเพิ่มเติมด้านล่าง

Java bytecode เทียบเท่า reverseString()

ฉันตรวจสอบรหัสไบต์สำหรับวิธีการของคุณในKotlinและJAVAตามลำดับ:

วิธี Kotlin bytecode ใน JAVA

...
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 bytecode ใน 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()ในKotlinรุ่น
  2. ดัชนีด้านซ้ายและขวาในวิธีJAVAจะเพิ่มขึ้นในขณะที่ดัชนีKotlinใหม่จะถูกสร้างขึ้นสำหรับการโทรซ้ำแต่ละครั้ง

ดังนั้นเรามาทดสอบว่า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)
}

สำหรับJAVAการทดสอบประสบความสำเร็จได้โดยไม่มีปัญหาในขณะที่สำหรับKotlinStackOverflowErrorมันล้มเหลวอย่างน่าสังเวชเนื่องจากการ อย่างไรก็ตามหลังจากที่ผมเพิ่มIntrinsics.checkParameterIsNotNull(s, "s")ไปJAVAวิธีการมันล้มเหลวเช่นกัน:

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

ข้อสรุป

คุณKotlinวิธีการมีความลึก recursion ที่มีขนาดเล็กที่สุดเท่าที่มันจะเรียกIntrinsics.checkParameterIsNotNull(s, "s")ในทุกขั้นตอนและทำให้หนักกว่าของJAVAคู่ หากคุณไม่ต้องการวิธีการที่สร้างขึ้นอัตโนมัตินี้คุณสามารถปิดการใช้งานการตรวจสอบโมฆะในระหว่างการรวบรวมตามคำตอบที่นี่

อย่างไรก็ตามเนื่องจากคุณเข้าใจถึงประโยชน์ที่tailrecจะได้รับ(แปลงสายเรียกซ้ำของคุณเป็นสายซ้ำ) คุณควรใช้สายนั้น


@ user207421 Intrinsics.checkParameterIsNotNull(...)ทุกวิธีการอุทธรณ์จะมีกรอบสแต็คของตัวเองรวมทั้ง เห็นได้ชัดว่าสแต็คเฟรมแต่ละเฟรมต้องการหน่วยความจำจำนวนหนึ่ง (สำหรับLocalVariableTableและตัวถูกดำเนินการและอื่น ๆ ) ..
Anatolii

0

Kotlin เป็นเพียงส่วนเล็ก ๆ ที่หิวมากขึ้น (Int object params io int params) นอกจากโซลูชัน tailrec ที่ตรงกับที่นี่แล้วคุณสามารถกำจัดตัวแปรโลคัลtempโดย xor-ing:

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

ไม่แน่ใจว่าจะใช้การลบตัวแปรในเครื่องหรือไม่

การกำจัด j อาจทำได้เช่นกัน:

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)
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.