출력 -1은 루프에서 슬래시가됩니다


54

놀랍게도 다음 코드가 출력됩니다.

/
-1

코드:

public class LoopOutPut {

    public static void main(String[] args) {
        LoopOutPut loopOutPut = new LoopOutPut();
        for (int i = 0; i < 30000; i++) {
            loopOutPut.test();
        }

    }

    public void test() {
        int i = 8;
        while ((i -= 3) > 0) ;
        String value = i + "";
        if (!value.equals("-1")) {
            System.out.println(value);
            System.out.println(i);
        }
    }

}

나는 이것이 몇 번이나 일어날지를 결정하기 위해 여러 번 시도했지만 불행히도 궁극적으로 불확실했으며 -2의 출력이 때때로 기간으로 바뀌는 것을 알았습니다. 또한 while 루프와 출력 -1을 문제없이 제거하려고했습니다. 누가 그 이유를 말해 줄 수 있습니까?


JDK 버전 정보 :

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1

2
의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Samuel Liew

답변:


36

이것은 openjdk version "1.8.0_222"(내 분석에 사용 된), OpenJDK 12.0.1(Oleksandr Pyrohov에 따라) 및 OpenJDK 13 (Carlos Heuberger에 따라)을 사용하여 안정적으로 재현 (또는 원하는 것에 따라 재현하지 않음) 할 수 있습니다 .

나는 -XX:+PrintCompilation두 가지 행동을 모두 취할 수 있는 충분한 시간으로 코드를 실행했으며 여기에 차이점이 있습니다.

버기 구현 (출력 표시) :

 --- Previous lines are identical in both
 54   17       3       java.lang.AbstractStringBuilder::<init> (12 bytes)
 54   23       3       LoopOutPut::test (57 bytes)
 54   18       3       java.lang.String::<init> (82 bytes)
 55   21       3       java.lang.AbstractStringBuilder::append (62 bytes)
 55   26       4       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
 55   20       3       java.lang.StringBuilder::<init> (7 bytes)
 56   19       3       java.lang.StringBuilder::toString (17 bytes)
 56   25       3       java.lang.Integer::getChars (131 bytes)
 56   22       3       java.lang.StringBuilder::append (8 bytes)
 56   27       4       java.lang.String::equals (81 bytes)
 56   10       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   made not entrant
 56   28       4       java.lang.AbstractStringBuilder::append (50 bytes)
 56   29       4       java.lang.String::getChars (62 bytes)
 56   24       3       java.lang.Integer::stringSize (21 bytes)
 58   14       3       java.lang.String::getChars (62 bytes)   made not entrant
 58   33       4       LoopOutPut::test (57 bytes)
 59   13       3       java.lang.AbstractStringBuilder::append (50 bytes)   made not entrant
 59   34       4       java.lang.Integer::getChars (131 bytes)
 60    3       3       java.lang.String::equals (81 bytes)   made not entrant
 60   30       4       java.util.Arrays::copyOfRange (63 bytes)
 61   25       3       java.lang.Integer::getChars (131 bytes)   made not entrant
 61   32       4       java.lang.String::<init> (82 bytes)
 61   16       3       java.util.Arrays::copyOfRange (63 bytes)   made not entrant
 61   31       4       java.lang.AbstractStringBuilder::append (62 bytes)
 61   23       3       LoopOutPut::test (57 bytes)   made not entrant
 61   33       4       LoopOutPut::test (57 bytes)   made not entrant
 62   35       3       LoopOutPut::test (57 bytes)
 63   36       4       java.lang.StringBuilder::append (8 bytes)
 63   18       3       java.lang.String::<init> (82 bytes)   made not entrant
 63   38       4       java.lang.StringBuilder::append (8 bytes)
 64   21       3       java.lang.AbstractStringBuilder::append (62 bytes)   made not entrant

올바른 실행 (표시 없음) :

 --- Previous lines identical in both
 55   23       3       LoopOutPut::test (57 bytes)
 55   17       3       java.lang.AbstractStringBuilder::<init> (12 bytes)
 56   18       3       java.lang.String::<init> (82 bytes)
 56   20       3       java.lang.StringBuilder::<init> (7 bytes)
 56   21       3       java.lang.AbstractStringBuilder::append (62 bytes)
 56   26       4       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
 56   19       3       java.lang.StringBuilder::toString (17 bytes)
 57   22       3       java.lang.StringBuilder::append (8 bytes)
 57   24       3       java.lang.Integer::stringSize (21 bytes)
 57   25       3       java.lang.Integer::getChars (131 bytes)
 57   27       4       java.lang.String::equals (81 bytes)
 57   28       4       java.lang.AbstractStringBuilder::append (50 bytes)
 57   10       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   made not entrant
 57   29       4       java.util.Arrays::copyOfRange (63 bytes)
 60   16       3       java.util.Arrays::copyOfRange (63 bytes)   made not entrant
 60   13       3       java.lang.AbstractStringBuilder::append (50 bytes)   made not entrant
 60   33       4       LoopOutPut::test (57 bytes)
 60   34       4       java.lang.Integer::getChars (131 bytes)
 61    3       3       java.lang.String::equals (81 bytes)   made not entrant
 61   32       4       java.lang.String::<init> (82 bytes)
 62   25       3       java.lang.Integer::getChars (131 bytes)   made not entrant
 62   30       4       java.lang.AbstractStringBuilder::append (62 bytes)
 63   18       3       java.lang.String::<init> (82 bytes)   made not entrant
 63   31       4       java.lang.String::getChars (62 bytes)

한 가지 중요한 차이점을 알 수 있습니다. 올바른 실행으로 test()두 번 컴파일 합니다. 처음에는 한 번, 그 후에는 다시 한 번 (JIT가 방법이 얼마나 뜨겁다는 것을 알기 때문에) 버그 test()가 있는 실행 은 5 번 컴파일 (또는 디 컴파일) 됩니다.

또한 with -XX:-TieredCompilation(해석 또는 사용 C2) 또는 with -Xbatch(컴파일을 병렬 대신 메인 스레드에서 강제로 실행)로 실행하면 출력이 보장 되고 30000 회 반복하면 많은 내용이 인쇄되므로 C2컴파일러가 범인이되기 위해 이것은로 실행하여 확인되며 -XX:TieredStopAtLevel=1, C2출력 을 비활성화 하고 생성하지 않습니다 (레벨 4에서 중지하면 다시 버그가 표시됨).

올바른 실행에서 메소드는 먼저 레벨 3 컴파일로 컴파일 된 후 레벨 4로 컴파일됩니다.

버기 실행에서 이전 컴파일은 무시되고 ( made non entrant) 레벨 3에서 다시 컴파일됩니다 (즉 C1, 이전 링크 참조).

C2레벨 3 컴파일로 되돌아 간다는 사실이 그 영향을 받는지 (그리고 왜 레벨 3으로 돌아가는지, 여전히 많은 불확실성이 있는지) 확실하지 않지만 확실하게 버그입니다 .

다음 줄을 사용하여 조립품 코드를 생성하여 토끼 구멍으로 깊숙이 들어갈 수 있습니다 (조립품 인쇄를 활성화 하려면 내용도 참조하십시오 ).

java -XX:+PrintCompilation -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoopOutPut > broken.asm

이 시점에서 기술이 부족해지기 시작합니다. 이전 컴파일 된 버전을 버릴 때 버그 동작이 나타나기 시작하지만 90 년대의 조립 기술이 거의 없기 때문에 나보다 똑똑한 사람을 보자 여기에서.

코드가 다른 사람에 의해 OP에 제시되었고 모든 코드 C2에 버그가 없기 때문에 이미 이것에 대한 버그 보고서가있을 수 있습니다 . 이 분석이 다른 사람에게 유익한 정보가되기를 바랍니다.

유능한 아파 긴이 주석에서 지적했듯이, 이것은 최근의 버그 입니다. 모든 관심 있고 도움이되는 사람들에게 많은 의무가 있습니다. :)


또한 C2JitWatch를 사용하여 생성 된 어셈블러 코드를 살펴보고 이해하려고 시도했습니다. C1생성 된 코드는 여전히 바이트 코드와 비슷하지만 C2완전히 다릅니다 ( i8으로 초기화를 찾을
수조차 없음

당신의 대답은 매우 좋습니다. 나는 시도했습니다 .c2를 비활성화하십시오. 결과는 정확합니다. 그러나 실제 프로젝트에는 위의 코드가 없지만 일반적으로 이러한 매개 변수의 대부분은 기본값이지만 프로젝트에서 유사한 코드를 사용하는 경우 유사한 코드를 가질 가능성이 있습니다
okali

1
@Eugene 이것은 매우 까다로운 것으로, 이클립스 컴파일러 버그 또는 이와 유사한 것이 될 것이라고 확신했습니다 ... 그리고 처음에는 그것을 재현 할 수 없었습니다 ..
Kayaman

1
@Kayaman은 동의했다. 당신이 만든 분석은 매우 훌륭합니다. 아 판긴이 이것을 설명하고 고칠 수있을만큼 충분해야합니다. 기차에서 얼마나 멋진 아침!
Eugene

7
이 주제는 우연히 만 나타났습니다. 질문을 확인하려면 @mentions를 사용하거나 #jvm 태그를 추가하십시오. 좋은 분석, BTW. 이것은 실제로 며칠 전에 수정 된 C2 컴파일러 버그입니다 -JDK-8231988 .
apangin

4

이 코드는 기술적으로 출력되지 않아야하기 때문에 솔직히 꽤 이상합니다 ...

int i = 8;
while ((i -= 3) > 0);

... 항상 발생한다 i-1(8 - 6 = 5; 5 - (3) = 2, 2 - 3 = -1). 더 이상한 것은 IDE의 디버그 모드에서 출력되지 않는다는 것입니다.

흥미롭게도로 변환하기 전에 확인 표시를 추가 한 순간 String아무런 문제가 없습니다 ...

public void test() {
  int i = 8;
  while ((i -= 3) > 0);
  if(i != -1) { System.out.println("Not -1"); }
  String value = String.valueOf(i);
  if (!"-1".equalsIgnoreCase(value)) {
    System.out.println(value);
    System.out.println(i);
  }
}

좋은 코딩 연습의 두 가지 점 ...

  1. 오히려 사용 String.valueOf()
  2. 일부 코딩 표준은 문자열 리터럴 .equals()이 인수가 아닌 의 대상이어야 하므로 NullPointerException을 최소화 하도록 지정합니다 .

이 문제가 발생하지 않는 유일한 방법은 String.format()

public void test() {
  int i = 8;
  while ((i -= 3) > 0);
  String value = String.format("%d", i);
  if (!"-1".equalsIgnoreCase(value)) {
    System.out.println(value);
    System.out.println(i);
  }
}

... 본질적으로 Java가 숨을 쉴 시간이 조금 필요한 것처럼 보입니다. :)

편집 : 이것은 우연의 일치 일 수도 있지만 인쇄하는 값과 ASCII Table 사이에는 약간의 일치가있는 것 같습니다 .

  • i= -1, 표시되는 문자는 /(ASCII 10 진수 값 47)
  • i= -2, 표시되는 문자는 .(ASCII 10 진수 값 46)
  • i= -3, 표시되는 문자는 -(ASCII 10 진수 값 45)
  • i= -4, 표시되는 문자는 ,(ASCII 10 진수 값 44)
  • i= -5, 표시되는 문자는 +(ASCII 10 진수 값 43)
  • i= -6, 표시되는 문자는 *(ASCII 10 진수 값 42)
  • i= -7, 표시되는 문자는 )(ASCII 10 진수 값 41)
  • i= -8, 표시되는 문자는 ((ASCII 10 진수 값 40)입니다.
  • i= -9, 표시되는 문자는 '(ASCII 10 진수 값 39)

실제로 흥미로운 것은 ASCII 십진수 48의 문자가 값 0이고 48-1 = 47 (문자 /) 등입니다.


1
문자 "/"의 수치는 "-1"이다. ??? 어디에서 왔습니까? ( (int)'/' == 47, (char)-1정의되지 0xFFFF는 <아닌 문자> 유니)
user85421 - 금지

1
문자 c = '/'; int a = Character.getNumericValue (c); System.out.println (a);
Ambro-r

어떻게 getNumericValue()주어진 코드 관련 ??? 그리고 어떻게 변환 않습니다 -1'/'??? 왜에 '-', getNumericValue('-')또한 -1??? (BTW는 많은 메소드를 반환합니다 -1)
user85421-19

@CarlosHeuberger, 내가 실행중인 getNumericValue()value( /문자 값을 얻기 위해). ASCII 10 진수 값 /이 47이어야한다는 것을 100 % 정확 하지만 (추가 getNumericValue()로 기대했던대로) -1을 반환했습니다 System.out.println(Character.getNumericValue(value.toCharArray()[0]));. 혼란스러워서 게시물을 업데이트했습니다.
Ambro-r

1

Java가 왜 임의의 출력을 제공하는지 알지 못하지만 루프 i내부의 더 큰 값에 대해서는 연결에 문제가 있습니다 for.

당신은 교체하는 경우 String value = i + "";와 라인을String value = String.valueOf(i) ; 코드가 작동 예상대로.

+int를 문자열로 변환하는 데 사용 하는 연결 은 기본이며 버그가있을 수 있습니다 (이상하게도 현재 발견하고 있음).

참고 : for 루프 내부의 i 값을 10000으로 줄였고 +연결 문제에 직면하지 않았습니다 .

이 문제는 Java 이해 관계자에게보고해야하며 이에 대한 의견을 제시 할 수 있습니다.

편집 for 루프의 i 값을 3 백만으로 업데이트하고 다음과 같이 새로운 오류 세트를 보았습니다.

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
    at java.lang.Integer.getChars(Integer.java:463)
    at java.lang.Integer.toString(Integer.java:402)
    at java.lang.String.valueOf(String.java:3099)
    at solving.LoopOutPut.test(LoopOutPut.java:16)
    at solving.LoopOutPut.main(LoopOutPut.java:8)

내 Java 버전은 8입니다.


1
나는 문자열 연결이 네이티브라고 생각하지 않습니다. 단지 StringConcatFactory(OpenJDK 13) 또는 StringBuilder(Java 8)을 사용합니다.
user85421-Banned

@CarlosHeuberger 가능합니다. StringConcatFactory 클래스 9이어야하는 경우 Java 9에서 나온 것 같습니다 . 그러나 내가 자바까지 아는 한 Java 8 java don; t 연산자 오버로드를 지원하지
않음

@Vinay, 이것도 시도했지만 작동하지만 루프를 30000에서 3000000으로 늘리는 순간 같은 문제가 발생합니다.
Ambro-r

@ Ambro-r 제안 된 값으로 시도했지만 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1오류가 발생합니다. 이상한.
Vinay Prajapati

3
i + ""정확하게 컴파일 된 new StringBuilder().append(i).append("").toString()자바 8, 결국 또한 출력을 생산하고 사용
user85421-금지
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.