Pemanggilan metode rekursif menyebabkan StackOverFlowError di kotlin tetapi tidak di java

14

Saya punya dua kode yang hampir sama di java dan kotlin

Jawa:

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

Kode java lulus tes dengan input besar tetapi kode kotlin menyebabkan StackOverFlowErrorkecuali saya menambahkan tailreckata kunci sebelum helperfungsi di kotlin.

Saya ingin tahu mengapa fungsi ini bekerja di java dan juga di kolin dengan tailrectetapi tidak di kotlin tanpa tailrec?

PS: Saya tahu apa yang tailrecharus dilakukan

hamid_c
sumber
1
Ketika saya menguji ini, saya menemukan bahwa versi Java akan bekerja untuk ukuran array hingga sekitar 29500, tetapi versi Kotlin akan berhenti sekitar 18500. Itu perbedaan yang signifikan, tetapi bukan yang sangat besar. Jika Anda perlu ini berfungsi untuk array besar, satu-satunya solusi yang baik adalah menggunakan tailrec, atau menghindari rekursi; ukuran tumpukan yang tersedia bervariasi antara proses, antara JVM dan pengaturan, dan tergantung pada metode dan paramsnya. Tetapi jika Anda bertanya karena penasaran murni (alasan yang sangat bagus!), Maka saya tidak yakin. Anda mungkin perlu melihat bytecode.
gidds

Jawaban:

7

Saya ingin tahu mengapa fungsi ini bekerja di java dan juga di kotlin dengan tailrectetapi tidak di kotlin tanpatailrec ?

Jawaban singkatnya adalah karena metode Kotlin Anda "lebih berat" daripada yang JAVA . Pada setiap panggilan itu memanggil metode lain yang "memprovokasi"StackOverflowError . Jadi, lihat penjelasan lebih rinci di bawah ini.

Setara bytecode Java untuk reverseString()

Saya memeriksa kode byte untuk metode Anda di Kotlin dan JAVA sesuai:

Metode bytecode Kotlin di 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);
    }
}
...

Metode bytecode JAVA di 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);
    }
}
...

Jadi, ada 2 perbedaan utama:

  1. Intrinsics.checkParameterIsNotNull(s, "s")dipanggil untuk masing - masing helper()dalam versi Kotlin .
  2. Indeks kiri dan kanan dalam metode JAVA bertambah, sementara di Kotlin indeks baru dibuat untuk setiap panggilan rekursif.

Jadi, mari kita uji seberapa Intrinsics.checkParameterIsNotNull(s, "s")sendirian mempengaruhi perilaku.

Uji kedua implementasi

Saya telah membuat tes sederhana untuk kedua kasus:

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

Dan

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

Untuk JAVA tes berhasil tanpa masalah sedangkan untuk Kotlin gagal total karena a StackOverflowError. Namun, setelah saya menambahkan Intrinsics.checkParameterIsNotNull(s, "s")ke metode JAVA gagal juga:

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

Kesimpulan

Metode Kotlin Anda memiliki kedalaman rekursi yang lebih kecil karena memanggil Intrinsics.checkParameterIsNotNull(s, "s")pada setiap langkah dan karenanya lebih berat daripada rekan JAVA- nya. Jika Anda tidak ingin metode yang dibuat secara otomatis ini, maka Anda dapat menonaktifkan pemeriksaan nol selama kompilasi seperti yang dijawab di sini

Namun, karena Anda memahami manfaat apa yang tailrecmembawa (mengubah panggilan rekursif Anda menjadi iteratif), Anda harus menggunakannya.

Anatolii
sumber
@ user207421 setiap pemanggilan metode memiliki bingkai tumpukan sendiri termasuk Intrinsics.checkParameterIsNotNull(...). Jelas, setiap susunan kerangka seperti itu membutuhkan sejumlah memori (untuk LocalVariableTabletumpukan operan dan sebagainya) ..
Anatolii
0

Kotlin hanya sedikit lebih lapar (Int objek params dan params). Selain solusi tailrec yang cocok di sini, Anda dapat menghilangkan variabel lokal tempdengan 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)
}

Tidak sepenuhnya yakin apakah ini berfungsi untuk menghapus variabel lokal.

Juga menghilangkan j mungkin dilakukan:

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)
}
Joop Eggen
sumber