Rekursiver Methodenaufruf verursacht StackOverFlowError in Kotlin, aber nicht in Java

14

Ich habe zwei fast identische Codes in Java und 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)
}

Der Java-Code besteht den Test mit einer großen Eingabe, aber der Kotlin-Code verursacht ein, es StackOverFlowErrorsei denn, ich habe tailrecvor der helperFunktion in Kotlin ein Schlüsselwort hinzugefügt.

Ich möchte wissen, warum diese Funktion in Java und auch in Kolin mit, tailrecaber nicht in Kotlin ohne funktioniert tailrec.

PS: Ich weiß was zu tailrectun ist

hamid_c
quelle
1
Als ich diese testete, stellte ich fest, dass die Java-Version für Array-Größen bis zu etwa 29500 funktionieren würde, die Kotlin-Version jedoch um 18500 aufhören würde. Das ist ein bedeutender Unterschied, aber kein sehr großer. Wenn dies für große Arrays erforderlich ist, besteht die einzig gute Lösung darin tailrec, eine Rekursion zu verwenden oder zu vermeiden. Die verfügbare Stapelgröße variiert zwischen Läufen, zwischen JVMs und Setups und hängt von der Methode und ihren Parametern ab. Aber wenn Sie aus purer Neugier fragen (ein guter Grund!), Dann bin ich mir nicht sicher. Sie müssten sich wahrscheinlich den Bytecode ansehen.
Gidds

Antworten:

7

Ich möchte wissen, warum diese Funktion in Java und auch in Kotlin mit, tailrecaber nicht in Kotlin ohne funktionierttailrec .

Die kurze Antwort lautet, dass Ihre Kotlin- Methode "schwerer" ist als die JAVA- Methode . Bei jedem Aufruf wird eine andere Methode aufgerufen, die "provoziert" StackOverflowError. Eine ausführlichere Erklärung finden Sie weiter unten.

Java-Bytecode-Äquivalente für reverseString()

Ich habe den Bytecode für Ihre Methoden in Kotlin und JAVA entsprechend überprüft :

Bytecode der Kotlin-Methode 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 der JAVA-Methode 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);
    }
}
...

Es gibt also zwei Hauptunterschiede:

  1. Intrinsics.checkParameterIsNotNull(s, "s")wird für jeden helper()in der aufgerufen Kotlin- Version .
  2. Linke und rechte Indizes in der JAVA- Methode werden inkrementiert, während in Kotlin für jeden rekursiven Aufruf neue Indizes erstellt werden.

Testen wir also, wie Intrinsics.checkParameterIsNotNull(s, "s") allein das Verhalten auswirkt.

Testen Sie beide Implementierungen

Ich habe für beide Fälle einen einfachen Test erstellt:

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

Und

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

Für JAVA war der Test ohne Probleme erfolgreich, während er für Kotlin aufgrund von a kläglich fehlschlug StackOverflowError. Nachdem ich Intrinsics.checkParameterIsNotNull(s, "s")die JAVA- Methode hinzugefügt habe , ist dies ebenfalls fehlgeschlagen:

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

Fazit

Ihre Kotlin- Methode hat eine geringere Rekursionstiefe, da sie Intrinsics.checkParameterIsNotNull(s, "s")bei jedem Schritt aufgerufen wird und daher schwerer als ihre JAVA ist Gegenstück. Wenn Sie diese automatisch generierte Methode nicht möchten, können Sie die Nullprüfungen während der Kompilierung deaktivieren, wie hier beantwortet

Da Sie jedoch verstehen, welchen Nutzen dies tailrecbringt (wandelt Ihren rekursiven Aufruf in einen iterativen um), sollten Sie diesen verwenden.

Anatolii
quelle
@ user207421 Jeder Methodenaufruf hat einen eigenen Stapelrahmen einschließlich Intrinsics.checkParameterIsNotNull(...). Offensichtlich benötigt jeder solche Stapelrahmen eine bestimmte Menge an Speicher (für den LocalVariableTableund Operandenstapel usw.).
Anatolii
0

Kotlin ist nur ein bisschen stapelhungriger (Int object params io int params). Neben der hier passenden tailrec-Lösung können Sie die lokale Variable tempdurch xor-ing entfernen :

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

Nicht ganz sicher, ob dies funktioniert, um eine lokale Variable zu entfernen.

Auch das Eliminieren von j könnte Folgendes bewirken:

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
quelle