Có trình biên dịch nào cho JVM sử dụng goto rộng rãi không?


47

Tôi nghĩ rằng hầu hết các bạn đều biết đó gotolà một từ khóa dành riêng trong ngôn ngữ Java nhưng không thực sự được sử dụng. Và có lẽ bạn cũng biết đó gotolà một opcode Java Virtual Machine (JVM). Tôi nghĩ tất cả các cấu trúc kiểm soát dòng chảy phức tạp của Java, Scala và Kotlin được, ở mức JVM, thực hiện sử dụng một số sự kết hợp của gotoifeq, ifle, ifltvv

Nhìn vào thông số JVM https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.goto_w tôi thấy cũng có một goto_wmã opcode. Trong khi đó gotocó phần bù nhánh 2 byte, goto_wlấy phần bù nhánh 4 byte. Thông số kỹ thuật nói rằng

Mặc dù lệnh goto_w có độ lệch nhánh 4 byte, các yếu tố khác giới hạn kích thước của phương thức là 65535 byte (§4.11). Giới hạn này có thể được nêu ra trong một bản phát hành tương lai của Máy ảo Java.

Nghe có vẻ như tôi goto_wlà bằng chứng trong tương lai, giống như một số các *_wopcode khác . Nhưng nó cũng xảy ra với tôi rằng có lẽ có goto_wthể được sử dụng với hai byte quan trọng hơn bằng 0 và hai byte ít quan trọng hơn giống như đối với goto, với các điều chỉnh khi cần thiết.

Ví dụ, được đưa ra Trường hợp chuyển đổi Java này (hoặc Scala Match-Case):

     12: lookupswitch  {
                112785: 48 // case "red"
               3027034: 76 // case "green"
              98619139: 62 // case "blue"
               default: 87
          }
      48: aload_2
      49: ldc           #17                 // String red
      51: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      54: ifeq          87
      57: iconst_0
      58: istore_3
      59: goto          87
      62: aload_2
      63: ldc           #19                 // String green
      65: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      68: ifeq          87
      71: iconst_1
      72: istore_3
      73: goto          87
      76: aload_2
      77: ldc           #20                 // String blue
      79: invokevirtual #18 
      // etc.

chúng ta có thể viết lại như

     12: lookupswitch  { 
                112785: 48
               3027034: 78
              98619139: 64
               default: 91
          }
      48: aload_2
      49: ldc           #17                 // String red
      51: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      54: ifeq          91 // 00 5B
      57: iconst_0
      58: istore_3
      59: goto_w        91 // 00 00 00 5B
      64: aload_2
      65: ldc           #19                 // String green
      67: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      70: ifeq          91
      73: iconst_1
      74: istore_3
      75: goto_w          91
      79: aload_2
      81: ldc           #20                 // String blue
      83: invokevirtual #18 
      // etc.

Tôi thực sự đã không thử điều này, vì có lẽ tôi đã mắc lỗi khi thay đổi "số dòng" để phù hợp với goto_ws. Nhưng vì nó nằm trong thông số kỹ thuật, nên có thể làm được.

Câu hỏi của tôi là liệu có một lý do nào đó mà trình biên dịch hoặc trình tạo mã byte khác có thể sử dụng goto_wvới giới hạn 65535 hiện tại ngoài việc cho thấy rằng nó có thể được thực hiện không?

Câu trả lời:


51

Kích thước của mã phương thức có thể lớn tới 64K.

Giá trị bù nhánh ngắn gotolà số nguyên 16 bit đã ký: từ -32768 đến 32767.

Vì vậy, phần bù ngắn không đủ để thực hiện bước nhảy từ đầu phương thức 65K đến hết.

Thậm chí javacđôi khi phát ra goto_w. Đây là một ví dụ:

public class WideGoto {

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000_000; ) {
            i += 123456;
            // ... repeat 10K times ...
        }
    }
}

Dịch ngược với javap -c:

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: ldc           #2
       5: if_icmplt     13
       8: goto_w        50018     // <<< Here it is! A jump to the end of the loop
          ...

// ... repeat 10K times ...Điều đó biên dịch? Tôi biết có giới hạn về kích thước của một lớp nguồn duy nhất ... nhưng tôi không biết chính xác nó là gì (tạo mã là lần duy nhất tôi thấy thứ gì đó thực sự đánh vào nó).
Elliott Frisch

3
@ElliottFrisch Nó làm. Miễn là kích thước mã byte của phương thức không vượt quá 65535 và chiều dài nhóm không đổi cũng nhỏ hơn 65535.
apangin

18
Mát mẻ. Cảm ơn. 64k nên là đủ cho bất cứ ai tôi đoán. ;)
Elliott Frisch

3
@ElliottFrisch - Lời khuyên mũ tham khảo.
TJ Crowder

34

Không có lý do để sử dụng goto_wkhi chi nhánh phù hợp với a goto. Nhưng dường như bạn đã bỏ lỡ rằng các nhánh là tương đối , sử dụng một phần bù có chữ ký, vì một nhánh cũng có thể đi lùi.

Bạn không nhận thấy nó khi nhìn vào đầu ra của một công cụ như thế javap, vì nó tính toán địa chỉ mục tiêu tuyệt đối kết quả trước khi in.

Vì vậy, gotophạm vi -327678 … +32767‬không phải lúc nào cũng đủ để giải quyết từng vị trí mục tiêu có thể có trong 0 … +65535phạm vi.

Ví dụ, phương pháp sau sẽ có một goto_whướng dẫn ở đầu:

public static void methodWithLargeJump(int i) {
    for(; i == 0;) {
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        } } } } } } } } } } } } } } } } } } } } 
    }
}
static void x() {}

Bản demo trên Ideone

Compiled from "Main.java"
class LargeJump {
  public static void methodWithLargeJump(int);
    Code:
       0: iload_0
       1: ifeq          9
       4: goto_w        57567
…

7
Wow, thật tuyệt vời. Dự án Java lớn nhất của tôi, với một vài gói và vài chục lớp giữa chúng, biên dịch thành gần 200KB. Nhưng của bạn Mainvới methodWithLargeJump()biên dịch tới gần 400KB.
Alonso del Arte

4
Điều đó chứng tỏ có bao nhiêu Java được tối ưu hóa cho trường hợp phổ biến ...
Holger

1
Làm thế nào bạn phát hiện ra rằng lạm dụng các bảng nhảy? Máy tạo mã?
Elliott Frisch

14
@ElliottFrisch Tôi chỉ phải nhớ rằng finallycác khối được sao chép cho luồng bình thường và đặc biệt (bắt buộc kể từ Java 6). Vì vậy, lồng nhau mười trong số chúng ngụ ý × 2¹⁰, sau đó, switch luôn có một mục tiêu mặc định, do đó, cùng với iload, nó cần mười byte cộng với phần đệm. Tôi cũng đã thêm một tuyên bố không cần thiết trong mỗi chi nhánh để ngăn chặn tối ưu hóa. Khai thác giới hạn là một chủ đề định kỳ, biểu thức lồng nhau , lambdas , lĩnh vực , nhà xây dựng ...
Holger

2
Thật thú vị, các biểu thức lồng nhau và rất nhiều hàm tạo cũng đánh vào các giới hạn triển khai của trình biên dịch, không chỉ các giới hạn mã byte. Ngoài ra còn có một câu hỏi và trả lời về kích thước tệp tối đa (có lẽ tôi vô tình nhớ câu trả lời của Tagir khi viết câu trả lời này). Cuối cùng, chiều dài tên gói tối đa và, về phía JVM, tối đa hóa lồng nhau . Có vẻ, mọi người cứ tò mò.
Holger

5

Dường như trong một số trình biên dịch (đã thử trong 1.6.0 và 11.0.7), nếu một phương thức đủ lớn thì cần goto_w, nó sử dụng riêng goto_w. Ngay cả khi nó có các bước nhảy rất cục bộ, nó vẫn sử dụng goto_w.


1
Tại sao có thể như vậy? Nó có phải là một cái gì đó để làm với bộ nhớ đệm hướng dẫn?
Alexander - Phục hồi Monica

@ Alexander-ReinstateMonica Có lẽ chỉ cần thực hiện dễ dàng.
David G.
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.