La chiamata al metodo ricorsivo causa StackOverFlowError in kotlin ma non in java


14

Ho due codici quasi identici in Java e Kotlin

Giava:

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

Il codice java supera il test con un input enorme ma il codice kotlin causa un a StackOverFlowErrormeno che non abbia aggiunto una tailrecparola chiave prima della helperfunzione in kotlin.

Voglio sapere perché questa funzione funziona in java e anche in kolin con tailrecma senza kotlin senza tailrec?

PS: so cosa tailrecfare


1
Quando li ho testati, ho scoperto che la versione Java avrebbe funzionato per dimensioni di array fino a circa 29500, ma la versione di Kotlin si sarebbe fermata intorno al 18500. Questa è una differenza significativa, ma non molto grande. Se è necessario che funzioni per array di grandi dimensioni, l'unica buona soluzione è utilizzare tailreco evitare la ricorsione; la dimensione dello stack disponibile varia tra le esecuzioni, tra JVM e configurazioni e in base al metodo e ai suoi parametri. Ma se lo chiedi per pura curiosità (una ragione perfettamente valida!), Non ne sono sicuro. Probabilmente avresti bisogno di guardare il bytecode.
saluta il

Risposte:


7

Voglio sapere perché questa funzione funziona in java e anche in kotlin con tailrecma non in kotlin senza tailrec?

La risposta breve è perché il tuo metodo Kotlin è "più pesante" di quello JAVA . Ad ogni chiamata chiama un altro metodo che "provoca" StackOverflowError. Quindi, vedi una spiegazione più dettagliata di seguito.

Equivalenti bytecode Java per reverseString()

Ho controllato il codice byte per i tuoi metodi in Kotlin e JAVA di conseguenza:

Bytecode del metodo Kotlin in 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);
    }
}
...

Bytecode del metodo JAVA in 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);
    }
}
...

Quindi, ci sono 2 differenze principali:

  1. Intrinsics.checkParameterIsNotNull(s, "s")viene invocato per ciascuno helper()nella versione di Kotlin .
  2. Gli indici sinistro e destro nel metodo JAVA vengono incrementati, mentre in Kotlin vengono creati nuovi indici per ogni chiamata ricorsiva.

Quindi, proviamo come Intrinsics.checkParameterIsNotNull(s, "s")solo influenza il comportamento.

Testare entrambe le implementazioni

Ho creato un semplice test per entrambi i casi:

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

E

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

Per JAVA il test ha avuto successo senza problemi mentre per Kotlin ha fallito miseramente a causa di a StackOverflowError. Tuttavia, dopo aver aggiunto Intrinsics.checkParameterIsNotNull(s, "s")al metodo JAVA anche questo ha avuto esito negativo:

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

Conclusione

Il tuo metodo Kotlin ha una profondità di ricorsione più piccola in quanto invoca Intrinsics.checkParameterIsNotNull(s, "s")ad ogni passo ed è quindi più pesante della sua controparte JAVA . Se non si desidera questo metodo generato automaticamente, è possibile disabilitare i controlli null durante la compilazione, come indicato qui

Tuttavia, poiché capisci quale vantaggio tailrecporta (converte la tua chiamata ricorsiva in una chiamata iterativa) dovresti usare quella.


@ user207421 ogni invocazione di metodo ha il proprio stack frame incluso Intrinsics.checkParameterIsNotNull(...). Ovviamente, ciascuno di questi frame dello stack richiede una certa quantità di memoria (per lo LocalVariableTablestack degli operandi e così via) ..
Anatolii

0

Kotlin è solo un po 'più affamato di stack (Int object params io int params). Oltre alla soluzione tailrec che si adatta qui, è possibile eliminare la variabile locale tempxor-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)
}

Non sono del tutto sicuro che funzioni per rimuovere una variabile locale.

Anche l'eliminazione di j potrebbe fare:

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)
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.