JVM의 컴파일러가 "wide"goto를 사용합니까?


47

나는 당신 goto이 Java 언어로 예약 된 키워드이지만 실제로 사용되지 않는다는 것을 대부분 알고 있습니다. 또한 gotoJVM (Java Virtual Machine) opcode 임을 알고있을 것입니다 . 나는 JVM 수준에서의 조합을 사용하여 구현하는 모든에게 자바, 스칼라와 코 틀린의 정교한 제어 흐름 구조를 이해 할수 gotoifeq, ifle, iflt, 등

JVM을 사양을 보면 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.goto_w 나는 거기에 또한 참조 goto_w연산 코드가. goto2 바이트 분기 오프셋을 사용하는 반면 goto_w4 바이트 분기 오프셋을 사용합니다. 사양은

있지만 goto_w 명령 오프셋 4 바이트 지점을 얻어, 다른 요인은 65,535 바이트 (§4.11)에있어서의 크기를 제한한다. 이 한계는 다음 릴리스의 Java Virtual Machine에서 발생할 수 있습니다.

goto_w다른 *_wopcode 와 마찬가지로 미래를 보장하는 것처럼 들립니다 . 그러나 필요에 따라 조정 goto_w하여 2 개의 더 중요한 바이트가 0으로 설정되고 2 개의 덜 중요한 바이트가와 동일하게 사용될 수 있다는 것도 나에게 발생 goto합니다.

예를 들어, 다음 Java 스위치 케이스 (또는 스칼라 매치 케이스)가 주어진 경우 :

     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.

우리는 그것을 다음과 같이 다시 쓸 수 있습니다

     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.

goto_ws 를 수용하기 위해 "줄 번호"를 변경하는 실수를했기 때문에 실제로 이것을 시도하지 않았습니다 . 그러나 그것은 사양에 있기 때문에 가능할 것입니다.

내 질문은 컴파일러 또는 다른 바이트 코드 생성기가 goto_w현재 65535 제한으로 사용할 수있는 이유가 있는지 여부입니다 .

답변:


51

메소드 코드의 크기는 64K만큼 클 수 있습니다.

short의 분기 오프셋은 goto-32768에서 32767 사이의 부호있는 16 비트 정수입니다.

따라서 짧은 오프셋은 65K 방법의 시작에서 끝으로 점프하기에 충분하지 않습니다.

심지어 javac때로는 방출한다 goto_w. 예를 들면 다음과 같습니다.

public class WideGoto {

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

로 디 컴파일 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 ...컴파일? 단일 소스 클래스의 크기에 제한이 있다는 것을 알고 있지만 정확히 무엇인지 알 수 없습니다 (코드 생성은 실제로 실제로 발생하는 것을 본 유일한 시간입니다).
엘리엇 프리쉬

3
@ElliottFrisch 그렇습니다. 메소드의 바이트 코드 크기가 65535를 초과하지 않고 상수 풀 길이도 65535보다 작은 한
apangin

18
멋있는. 감사. 64k는 내가 생각하는 사람이라면 충분할 것입니다. ;)
Elliott Frisch

3
@ElliottFrisch- 팁 모자 참조.
TJ Crowder

34

goto_w분기가에 맞을 때 사용할 이유가 없습니다 goto. 그러나 분기가 뒤로 갈 수 있기 때문에 부호있는 오프셋을 사용하여 분기가 상대적 인 것을 놓친 것 같습니다 .

javap인쇄하기 전에 결과 절대 목표 주소를 계산 하므로과 같은 도구의 출력을 볼 때이를 알 수 없습니다 .

그래서 goto의의 범위는 -327678 … +32767‬항상 각 가능한 대상 위치를 처리하는 것만으로는 충분하지 않습니다 0 … +65535범위.

예를 들어 다음 방법은 goto_w시작 부분에 지침 이 있습니다 .

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

이데온 데모

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

7
와우 패키지와 패키지 사이에 수십 개의 클래스가있는 가장 큰 Java 프로젝트는 거의 200KB로 컴파일됩니다. 그러나 당신 MainmethodWithLargeJump()거의 400KB 로 컴파일합니다.
Alonso del Arte

4
이는 일반적인 경우에 얼마나 많은 Java가 최적화되어 있는지 보여줍니다.
Holger

1
점프 테이블의 남용을 어떻게 발견 했습니까? 기계 생성 코드?
엘리엇 프리쉬

14
@ElliottFrisch 필자는 finally정상적이고 예외적 인 흐름 (Java 6부터 필수)을 위해 블록이 복제 된다는 것을 기억해야했습니다 . 따라서 열 개의 중첩은 × 2¹⁰을 의미하며 스위치는 항상 기본 대상을 가지므로 iload와 함께 10 바이트 + 패딩이 필요합니다. 또한 최적화를 방지하기 위해 각 분기에 사소한 설명을 추가했습니다. 악용 한계는 반복되는 주제, 중첩 된 표현 , 람다 , 필드 , 생성자입니다 .
Holger

2
흥미롭게도 중첩 식과 많은 생성자가 바이트 코드 제한뿐만 아니라 컴파일러 구현 제한에도 영향을줍니다. 최대 클래스 파일 크기 에 대한 Q & A도있었습니다 (아마도이 ​​답변을 쓸 때 Tagir의 답변을 무의식적으로 기억했습니다). 마지막으로 최대 패키지 이름 길이 및 JVM 측에서 최대 중첩 동기화 . 사람들은 계속 호기심을 유지하는 것 같습니다.
Holger

5

일부 컴파일러 (1.6.0 및 11.0.7에서 시도)에서 메소드가 goto_w를 필요로 할 정도로 충분히 크면 goto_w 만을 사용 하는 것으로 보입니다. 로컬 점프가 매우 많더라도 여전히 goto_w를 사용합니다.


1
왜 그런가요? 명령 캐싱과 관련이 있습니까?
Alexander-복원 자 Monica Monica

@ Alexander-ReinstateMonica 아마도 구현이 쉬울 것입니다.
David G.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.