Output -1 menjadi garis miring dalam loop

54

Anehnya, kode berikut ini menghasilkan:

/
-1

Kode:

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

}

Saya mencoba berkali-kali untuk menentukan berapa kali ini akan terjadi, tetapi, sayangnya, pada akhirnya tidak pasti, dan saya menemukan bahwa output -2 terkadang berubah menjadi suatu periode. Selain itu, saya juga mencoba menghapus while loop dan output -1 tanpa masalah. Siapa yang bisa memberi tahu saya alasannya?


Informasi versi JDK:

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1
okali
sumber
2
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Samuel Liew

Jawaban:

36

Ini dapat direproduksi secara andal (atau tidak direproduksi, tergantung pada apa yang Anda inginkan) dengan openjdk version "1.8.0_222"(digunakan dalam analisis saya), OpenJDK 12.0.1(menurut Oleksandr Pyrohov) dan OpenJDK 13 (menurut Carlos Heuberger).

Saya menjalankan kode dengan -XX:+PrintCompilationwaktu yang cukup untuk mendapatkan perilaku dan inilah perbedaannya.

Implementasi Buggy (menampilkan output):

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

Proses yang benar (tidak ada tampilan):

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

Kita dapat melihat satu perbedaan signifikan. Dengan eksekusi yang benar, kami mengkompilasi test()dua kali. Sekali di awal, dan sekali lagi sesudahnya (mungkin karena JIT memperhatikan betapa panas metode ini). Dalam eksekusi buggy test()dikompilasi (atau didekompilasi) 5 kali.

Selain itu, berjalan dengan -XX:-TieredCompilation(yang mengartikan, atau menggunakan C2) atau dengan -Xbatch(yang memaksa kompilasi untuk berjalan di utas utama, bukannya paralel), output dijamin dan dengan 30000 iterasi mencetak banyak hal, sehingga C2kompiler tampaknya untuk menjadi pelakunya. Ini dikonfirmasi dengan berjalan dengan -XX:TieredStopAtLevel=1, yang menonaktifkan C2dan tidak menghasilkan output (berhenti di level 4 menunjukkan bug lagi).

Dalam eksekusi yang benar, metode ini pertama kali dikompilasi dengan kompilasi Level 3 , kemudian setelah itu dengan Level 4.

Dalam eksekusi buggy, kompilasi sebelumnya diabaikan ( made non entrant) dan dikompilasi lagi pada Level 3 (yaitu C1, lihat tautan sebelumnya).

Jadi itu pasti bug C2, walaupun saya tidak benar-benar yakin apakah fakta bahwa itu akan kembali ke kompilasi Level 3 memengaruhinya (dan mengapa itu kembali ke level 3, masih banyak ketidakpastian masih).

Anda dapat membuat kode rakitan dengan baris berikut untuk masuk lebih dalam ke lubang kelinci (lihat juga ini untuk mengaktifkan pencetakan rakitan).

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

Pada titik ini saya mulai kehabisan keterampilan, perilaku buggy mulai menunjukkan ketika versi kompilasi sebelumnya dibuang, tetapi betapa sedikit keterampilan perakitan yang saya miliki dari tahun 90-an, jadi saya akan membiarkan seseorang yang lebih pintar daripada saya mengambilnya dari sini.

Kemungkinan sudah ada laporan bug tentang ini, karena kode tersebut disampaikan kepada OP oleh orang lain, dan karena semua kode C2 bukan tanpa bug . Saya berharap analisis ini bermanfaat bagi orang lain seperti juga bagi saya.

Seperti yang ditunjukkan oleh Yang Mulia Apangin dalam komentar, ini adalah bug baru - baru ini . Banyak yang diwajibkan untuk semua orang yang tertarik dan membantu :)

Kayaman
sumber
Saya juga berpikir itu C2- telah melihat kode assembler yang dihasilkan (dan mencoba memahaminya) menggunakan JitWatch - C1kode yang dihasilkan masih menyerupai bytecode, C2sama sekali berbeda (saya bahkan tidak bisa menemukan inisialisasi idengan 8)
user85421-Banned
jawaban Anda sangat bagus, saya mencoba, menonaktifkan c2, hasilnya benar. Namun, secara umum, sebagian besar parameter ini adalah default dalam proyek, meskipun proyek yang sebenarnya tidak akan memiliki kode di atas, tetapi kemungkinan memiliki kode yang sama, jika proyek menggunakan kode yang sama, itu benar-benar mengerikan
okali
1
@Eugene ini cukup rumit, saya yakin itu akan menjadi sesuatu seperti bug kompiler gerhana atau sejenisnya ... dan saya tidak dapat mereproduksinya pada awalnya juga ..
Kayaman
1
@ Kayaman setuju. Analisis yang Anda buat sangat bagus, seharusnya lebih dari cukup untuk apangin menjelaskan dan memperbaikinya. Pagi yang luar biasa di kereta!
Eugene
7
Saya memperhatikan topik ini hanya secara tidak sengaja. Untuk memastikan saya melihat pertanyaan, gunakan @mentions atau tambahkan tag #jvm. Analisis yang bagus, BTW. Ini memang bug kompiler C2, diperbaiki hanya beberapa hari yang lalu - JDK-8231988 .
apangin
4

Ini sejujurnya cukup aneh, karena kode itu secara teknis seharusnya tidak pernah keluar karena ...

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

... harus selalu menghasilkan imenjadi -1(8 - 3 = 5; 5 - 3 = 2; 2 - 3 = -1). Yang lebih aneh lagi, ia tidak pernah menghasilkan mode debug pada IDE saya.

Menariknya, saat saya menambahkan cek sebelum konversi ke String, maka tidak ada masalah ...

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

Hanya dua poin praktik pengkodean yang baik ...

  1. Sebaliknya gunakan String.valueOf()
  2. Beberapa standar pengkodean menetapkan bahwa String literal harus menjadi target .equals(), bukan argumen, sehingga meminimalkan NullPointerExceptions.

Satu-satunya cara saya mendapatkan ini tidak terjadi adalah dengan menggunakan 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);
  }
}

... pada dasarnya sepertinya Jawa membutuhkan sedikit waktu untuk menghirup napas :)

EDIT: Ini mungkin sepenuhnya kebetulan, tetapi tampaknya ada beberapa korespondensi antara nilai yang dicetak dan Tabel ASCII .

  • i= -1, karakter yang ditampilkan adalah /(nilai desimal ASCII 47)
  • i= -2, karakter yang ditampilkan adalah .(nilai desimal ASCII 46)
  • i= -3, karakter yang ditampilkan adalah -(nilai desimal ASCII 45)
  • i= -4, karakter yang ditampilkan adalah ,(nilai desimal ASCII 44)
  • i= -5, karakter yang ditampilkan adalah +(nilai desimal ASCII 43)
  • i= -6, karakter yang ditampilkan adalah *(nilai desimal ASCII 42)
  • i= -7, karakter yang ditampilkan adalah )(nilai desimal ASCII 41)
  • i= -8, karakter yang ditampilkan adalah ((nilai desimal ASCII 40)
  • i= -9, karakter yang ditampilkan adalah '(nilai desimal ASCII 39)

Apa yang benar-benar menarik adalah bahwa karakter pada ASCII desimal 48 adalah nilai 0dan 48 - 1 = 47 (karakter /), dll ...

Ambro-r
sumber
1
Apakah nilai numerik dari karakter "/" adalah "-1" ??? dari mana ini berasal? ( (int)'/' == 47; (char)-1tidak ditentukan 0xFFFFadalah <bukan karakter> di Unicode)
user85421-Banned
1
char c = '/'; int a = Character.getNumericValue (c); System.out.println (a);
Ambro-r
bagaimana getNumericValue()hubungannya dengan kode yang diberikan ??? dan bagaimana cara dikonversi -1ke '/'??? Kenapa tidak '-', getNumericValue('-')juga -1??? (BTW banyak metode kembali -1)
user85421-Banned
@CarlosHeuberger, aku berlari getNumericValue()pada value( /) untuk mendapatkan nilai karakter. Anda 100% benar bahwa nilai desimal ASCII /harus 47 (itu yang saya juga harapkan), tetapi getNumericValue()mengembalikan -1 pada titik itu seperti yang saya tambahkan System.out.println(Character.getNumericValue(value.toCharArray()[0]));. Saya dapat melihat kebingungan yang Anda maksudkan dan telah memperbarui posnya.
Ambro-r
1

Tidak tahu mengapa Java memberikan output acak seperti itu tetapi masalahnya ada pada rangkaian Anda yang gagal untuk nilai yang lebih besar idi dalam forloop.

Jika Anda mengganti String value = i + "";baris dengan String value = String.valueOf(i) ;kode Anda berfungsi seperti yang diharapkan.

Penggabungan menggunakan +untuk mengkonversi int ke string adalah asli dan mungkin buggy (Anehnya kita menemukan sekarang mungkin) dan menyebabkan masalah seperti itu.

Catatan: Saya mengurangi nilai i inside untuk loop ke 10000 dan saya tidak menghadapi masalah dengan +penggabungan.

Masalah ini harus dilaporkan kepada pemangku kepentingan Jawa & mereka dapat memberikan pendapat yang sama.

Sunting Saya memperbarui nilai i in untuk loop ke 3 juta dan melihat serangkaian kesalahan baru seperti di bawah ini:

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)

Versi Java saya adalah 8.

Vinay Prajapati
sumber
1
Saya tidak berpikir bahwa rangkaian string adalah asli - itu hanya menggunakan StringConcatFactory(OpenJDK 13) atau StringBuilder(Java 8)
user85421-Banned
@CarlosHeuberger Kemungkinan juga. Saya pikir ini dari java 9 jika harus StringConcatFactory kelas. tapi sejauh yang saya tahu java sampai java 8 java don; t mendukung operator overloading
Vinay Prajapati
@Vayay, coba ini juga dan ya itu berhasil, tetapi saat Anda meningkatkan loop dari 30000 untuk mengatakan 3000000 Anda mulai mengalami masalah yang sama.
Ambro-r
@ Ambro-r Saya mencoba dengan nilai yang Anda sarankan dan saya mendapatkan Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1kesalahan. Aneh.
Vinay Prajapati
3
i + ""dikompilasi persis seperti new StringBuilder().append(i).append("").toString()di Java 8, dan menggunakannya yang akhirnya juga menghasilkan output
user85421-Banned