Cuộc gọi phương thức đệ quy gây ra StackOverFlowError trong kotlin nhưng không phải trong java


14

Tôi có hai mã gần như giống hệt nhau trong java và 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)
}

Mã java vượt qua bài kiểm tra với đầu vào rất lớn nhưng mã kotlin gây ra StackOverFlowErrortrừ khi tôi thêm tailrectừ khóa trước helperhàm trong kotlin.

Tôi muốn biết tại sao chức năng này hoạt động trong java và cả trong kolin tailrecnhưng không có trong kotlin mà không có tailrec?

Tái bút: tôi biết tailrecphải làm gì


1
Khi tôi thử nghiệm những điều này, tôi thấy rằng phiên bản Java sẽ hoạt động với kích thước mảng lên tới khoảng 29500, nhưng phiên bản Kotlin sẽ dừng vào khoảng năm 18500. Đó là một sự khác biệt đáng kể, nhưng không phải là rất lớn. Nếu bạn cần điều này để làm việc cho các mảng lớn, giải pháp tốt duy nhất là sử dụng tailrechoặc tránh đệ quy; kích thước ngăn xếp có sẵn khác nhau giữa các lần chạy, giữa các JVM và các thiết lập, và tùy thuộc vào phương thức và các tham số của nó. Nhưng nếu bạn hỏi về sự tò mò thuần túy (một lý do hoàn toàn chính đáng!), Thì tôi không chắc chắn. Bạn có thể cần phải nhìn vào mã byte.
chơi

Câu trả lời:


7

Tôi muốn biết lý do tại sao chức năng này hoạt động trong java và cũng trong Kotlin với tailrecnhưng không phải trong Kotlin không tailrec?

Câu trả lời ngắn gọn là vì phương pháp Kotlin của bạn "nặng" hơn phương pháp JAVA . Tại mỗi cuộc gọi, nó gọi một phương thức khác là "khiêu khích" StackOverflowError. Vì vậy, xem một lời giải thích chi tiết hơn dưới đây.

Tương đương mã byte Java cho reverseString()

Tôi đã kiểm tra mã byte cho các phương thức của bạn trong KotlinJAVA tương ứng:

Mã phương thức Kotlin trong 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);
    }
}
...

Phương thức JAVA mã byte trong 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);
    }
}
...

Vì vậy, có 2 điểm khác biệt chính:

  1. Intrinsics.checkParameterIsNotNull(s, "s")được gọi cho mỗi helper()trong phiên bản Kotlin .
  2. Các chỉ mục bên trái và bên phải trong phương thức JAVA được tăng lên, trong khi trong Kotlin, các chỉ mục mới được tạo cho mỗi cuộc gọi đệ quy.

Vì vậy, hãy kiểm tra xem Intrinsics.checkParameterIsNotNull(s, "s")một mình ảnh hưởng đến hành vi như thế nào .

Kiểm tra cả hai triển khai

Tôi đã tạo một thử nghiệm đơn giản cho cả hai trường hợp:

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

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

Đối với JAVA , bài kiểm tra đã thành công mà không gặp vấn đề gì trong khi đối với Kotlin, nó đã thất bại thảm hại do a StackOverflowError. Tuy nhiên, sau khi tôi thêm Intrinsics.checkParameterIsNotNull(s, "s")vào JAVA phương pháp đó thất bại cũng như:

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

Phần kết luận

Phương pháp Kotlin của bạn có độ sâu đệ quy nhỏ hơn khi nó gọi Intrinsics.checkParameterIsNotNull(s, "s")ở mọi bước và do đó nặng hơn so với đối tác JAVA của nó . Nếu bạn không muốn phương thức được tạo tự động này thì bạn có thể tắt kiểm tra null trong quá trình biên dịch như đã trả lời ở đây

Tuy nhiên, vì bạn hiểu lợi ích tailrecmang lại (chuyển cuộc gọi đệ quy của bạn thành cuộc gọi lặp lại), bạn nên sử dụng cuộc gọi đó.


@ user207421 mỗi lời gọi phương thức có khung ngăn xếp riêng bao gồm Intrinsics.checkParameterIsNotNull(...). Rõ ràng, mỗi khung ngăn xếp như vậy đòi hỏi một lượng bộ nhớ nhất định (đối với LocalVariableTablengăn xếp và toán hạng, v.v.)
Anatolii

0

Kotlin chỉ là một đống đói nhỏ hơn một chút (Int object params io int params). Bên cạnh giải pháp tailrec phù hợp ở đây, bạn có thể loại bỏ biến cục bộ tempbằng 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)
}

Không hoàn toàn chắc chắn liệu điều này hoạt động để loại bỏ một biến cục bộ.

Cũng loại bỏ j có thể làm:

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)
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.