出力-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)

大きな違いが1つあります。正しく実行すると、test()2回コンパイルされます。最初に一度、その後もう一度(おそらくJITがメソッドがどれほどホットであるかを認識しているため)。バギーの実行でtest()5回コンパイル(または逆コンパイル)されます。

また、で実行されている-XX:-TieredCompilation(その解釈、または使用のいずれかC2または-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年代のものなので、私よりも賢い人にそれを取らせますここから。

すべてのコードC2にはバグがないわけではないので、コードは他の誰かによってOPに提示されたため、これに関するバグレポートがすでにある可能性があります。この分析が他の人にとっても私にとって有益であったことを願っています。

由緒あるアパンギンがコメントで指摘したように、これは最近のバグです。すべての興味深く、役立つ人々に義務づけられています:)


また、私はそれだと思うC2- JitWatch使用して生成されたアセンブラコードを見て(とそれを理解しようとした)している- C1、生成されたコードはまだバイトコードに似ているC2(私もの初期化見つけることができなかった全く異なるi8とを)
user85421-禁止

あなたの答えはとても良いです、私は試しました、c2を無効にします、結果は正しいです。ただし、一般に、これらのパラメーターのほとんどはプロジェクトのデフォルトですが、実際のプロジェクトには上記のコードはありませんが、プロジェクトに同様のコードが含まれている可能性が高く、プロジェクトが同様のコードを使用している場合、それは本当にひどい
okali

1
これはかなりトリッキー一つとなっている@Eugene、私は確かにそれは日食のコンパイラのバグまたは類似のようなものになるだろうした...と私はどちらか最初...でそれを再現することができませんでした
Kayaman

1
@カヤマンは同意した。あなたが作った分析はとても良いです、それはアパンギンがこれを説明して修正するのに十分以上でなければなりません。電車の中でなんと素晴らしい朝!
ユージーン

7
私はこのトピックに偶然に気づきました。質問が確実に表示されるようにするには、@ mentionsを使用するか、#jvmタグを追加します。良い分析、ところで。これは確かにC2コンパイラのバグであり、ほんの数日前に修正されました-JDK-8231988
apangin

4

なぜなら、そのコードは技術的に出力すべきではないからです...

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

...常にもたらすはずでiある-1(8 - = 5 3; 5 - = 2 3; 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);
  }
}

コーディングの実践における2つのポイント...

  1. むしろ使用 String.valueOf()
  2. 一部のコーディング標準では、文字列リテラルを.equals()引数ではなくのターゲットにするように指定しているため、NullPointerExceptionsが最小限に抑えられます。

これが起こらないようにした唯一の方法は、 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テーブルの間には何らかの対応があるようです。

  • 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 10進数48の文字が値で0あり、48-1 = 47(文字/)などであるということです。


1
文字「/」の数値は「-1」??? これはどこから来たのですか?((int)'/' == 47; (char)-1未定義0xFFFFはUnicodeの<文字ではない>)
user85421-Baned

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

getNumericValue()与えられたコードとどのように関係しますか?そしてそれはどのように変換さ-1れます'/'か?なぜないように'-'getNumericValue('-')またある-1??? (BTW多くのメソッドが返されます-1
user85421-19年

@CarlosHeuberger、私は()で実行getNumericValue()して文字値を取得していました。あなたは100%正解です。ASCII10進数値は47 である必要がありますが(これも私が期待していたものでした)、追加したときにその時点で-1を返していました。あなたが言及している混乱がわかり、投稿を更新しました。value//getNumericValue()System.out.println(Character.getNumericValue(value.toCharArray()[0]));
Ambro-r

1

なぜJavaがこのようなランダムな出力を出すのか分かりませんが、問題はiforループ内のより大きな値で失敗する連結にあります。

String value = i + "";行をString value = String.valueOf(i) ;コードに置き換えると、期待どおりに機能します。

+intを文字列に変換するために使用する連結はネイティブであり、バグがある可能性があり(奇妙なことに、おそらく現在、それを見つけている)、そのような問題を引き起こしています。

注:forループ内のiの値を10000に減らし、+連結の問題に直面しませんでした。

この問題はJavaの利害関係者に報告する必要があり、彼らは同じことについて意見を述べることができます。

編集私はforループのiの値を300万に更新し、以下のような新しいエラーセットを確認しました。

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-禁止

@CarlosHeuberger可能です。StringConcatFactory クラスでなければならないのであれば、Java 9からのものだと思います。しかし、私が知る限り、java 8までのjava javaは演算子のオーバーロードをサポートしていません
Vinay Prajapati

@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()Java 8とまったく同じようにコンパイルされ、それを使用すると最終的に出力も生成されます
user85421-Baned
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.