Memutuskan optimisasi JIT dengan refleksi

9

Ketika mengutak-atik unit test untuk kelas singleton yang sangat bersamaan, saya menemukan perilaku aneh berikut (diuji pada JDK 1.8.0_162):

private static class SingletonClass {
    static final SingletonClass INSTANCE = new SingletonClass(0);
    final int value;

    static SingletonClass getInstance() {
        return INSTANCE;
    }

    SingletonClass(int value) {
        this.value = value;
    }
}

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

    System.out.println(SingletonClass.getInstance().value); // 0

    // Change the instance to a new one with value 1
    setSingletonInstance(new SingletonClass(1));
    System.out.println(SingletonClass.getInstance().value); // 1

    // Call getInstance() enough times to trigger JIT optimizations
    for(int i=0;i<100_000;++i){
        SingletonClass.getInstance();
    }

    System.out.println(SingletonClass.getInstance().value); // 1

    setSingletonInstance(new SingletonClass(2));
    System.out.println(SingletonClass.INSTANCE.value); // 2
    System.out.println(SingletonClass.getInstance().value); // 1 (2 expected)
}

private static void setSingletonInstance(SingletonClass newInstance) throws NoSuchFieldException, IllegalAccessException {
    // Get the INSTANCE field and make it accessible
    Field field = SingletonClass.class.getDeclaredField("INSTANCE");
    field.setAccessible(true);

    // Remove the final modifier
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

    // Set new value
    field.set(null, newInstance);
}

2 baris terakhir dari metode main () tidak setuju pada nilai INSTANCE - tebakan saya adalah bahwa JIT menyingkirkan metode sepenuhnya karena bidang final statis. Menghapus kata kunci terakhir membuat kode menghasilkan nilai yang benar.

Mengesampingkan simpati Anda (atau ketiadaannya) untuk lajang dan lupa sejenak bahwa menggunakan refleksi seperti ini meminta masalah - apakah asumsi saya benar bahwa optimisasi JIT yang harus disalahkan? Jika demikian - apakah hanya terbatas pada bidang akhir statis?

Kelm
sumber
1
Singleton adalah kelas yang hanya ada satu instance. Oleh karena itu, Anda tidak memiliki singleton, Anda hanya memiliki kelas dengan static finalbidang. Selain itu, tidak masalah apakah retasan refleksi ini rusak karena JIT atau konkurensi.
Holger
@ Holger hack ini dilakukan dalam unit test hanya sebagai upaya untuk mengejek singleton untuk beberapa test case kelas yang menggunakannya. Saya tidak melihat bagaimana konkurensi dapat menyebabkannya (tidak ada dalam kode di atas) dan saya benar-benar ingin tahu apa yang terjadi.
Kelm
1
Nah, Anda mengatakan "kelas tunggal sangat konkuren" dalam pertanyaan Anda dan saya katakan " tidak masalah " apa yang membuatnya pecah. Jadi jika kode contoh khusus Anda rusak karena JIT dan Anda menemukan solusi untuk itu dan kemudian, kode nyata berubah dari melanggar karena JIT menjadi rusak karena konkurensi, apa yang telah Anda peroleh?
Holger
@ Holger oke, kata-katanya agak terlalu kuat, maaf soal itu. Yang saya maksudkan adalah ini - jika kita tidak mengerti mengapa sesuatu berjalan sangat salah, kita cenderung digigit oleh hal yang sama di masa depan, jadi saya lebih suka tahu alasannya daripada menganggap "itu terjadi begitu saja". Bagaimanapun, terima kasih telah meluangkan waktu Anda untuk menjawab!
Kelm

Jawaban:

7

Mengambil pertanyaan Anda secara harfiah, “ ... apakah asumsi saya benar dalam hal optimasi JIT yang harus disalahkan? ”, Jawabannya adalah ya, sangat mungkin bahwa optimasi JIT bertanggung jawab atas perilaku ini dalam contoh khusus ini.

Tetapi karena mengubah static finalbidang sama sekali tidak spesifik, ada hal lain yang dapat merusaknya dengan cara yang sama. Misalnya JMM tidak memiliki definisi untuk visibilitas memori dari perubahan tersebut, maka, itu sepenuhnya tidak ditentukan apakah atau ketika thread lain memperhatikan perubahan tersebut. Mereka bahkan tidak diharuskan untuk memperhatikannya secara konsisten, yaitu mereka dapat menggunakan nilai baru, diikuti dengan menggunakan nilai yang lama lagi, bahkan di hadapan primitif sinkronisasi.

Padahal, JMM dan optimizer sulit untuk dipisahkan di sini.

Pertanyaan Anda “ ... apakah hanya terbatas pada bidang akhir statis? ”Jauh lebih sulit untuk dijawab, karena optimisasi tentu saja tidak terbatas pada static finalbidang, tetapi perilaku, misalnya finalbidang non-statis , tidak sama dan memiliki perbedaan antara teori dan praktik juga.

Untuk finalbidang non-statis , modifikasi melalui Refleksi diperbolehkan dalam kondisi tertentu. Ini ditunjukkan oleh fakta bahwa setAccessible(true)cukup untuk membuat modifikasi seperti itu mungkin, tanpa meretas Fieldcontoh untuk mengubah modifiersbidang internal .

Spesifikasi mengatakan:

17.5.3. Modifikasi finalBidang Selanjutnya

Dalam beberapa kasus, seperti deserialization, sistem perlu mengubah finalbidang objek setelah konstruksi. finalbidang dapat diubah melalui refleksi dan cara lain yang bergantung pada implementasi. Satu-satunya pola di mana ini memiliki semantik yang masuk akal adalah pola di mana objek dibangun dan kemudian finalbidang objek diperbarui. Objek tidak boleh dibuat terlihat oleh utas lain, atau finalbidang tidak boleh dibaca, sampai semua pembaruan ke finalbidang objek selesai. Pembekuan finalbidang terjadi baik pada akhir konstruktor di mana finalbidang diatur, dan segera setelah setiap modifikasi finalbidang melalui refleksi atau mekanisme khusus lainnya.

...

Masalah lain adalah bahwa spesifikasi memungkinkan optimalisasi finalbidang secara agresif . Di dalam utas, diizinkan untuk menyusun ulang bacaan finalbidang dengan modifikasi finalbidang yang tidak terjadi di konstruktor.

Contoh 17.5.3-1. Optimalisasi finalBidang Agresif
class A {
    final int x;
    A() { 
        x = 1; 
    } 

    int f() { 
        return d(this,this); 
    } 

    int d(A a1, A a2) { 
        int i = a1.x; 
        g(a1); 
        int j = a2.x; 
        return j - i; 
    }

    static void g(A a) { 
        // uses reflection to change a.x to 2 
    } 
}

Dalam dmetode ini, kompiler diperbolehkan untuk menyusun ulang pembacaan xdan panggilan untuk gbebas. Dengan demikian, new A().f()bisa kembali -1, 0atau 1.

Dalam praktiknya, menentukan tempat yang tepat di mana optimisasi agresif dimungkinkan tanpa melanggar skenario hukum yang dijelaskan di atas, merupakan masalah terbuka , jadi kecuali -XX:+TrustFinalNonStaticFieldstelah ditentukan, JVM HotSpot tidak akan mengoptimalkan finalbidang non-statis dengan cara yang sama seperti static finalbidang.

Tentu saja, ketika Anda tidak mendeklarasikan isian sebagai final, JIT tidak dapat berasumsi bahwa itu tidak akan pernah berubah, meskipun, dengan tidak adanya primitif sinkronisasi ulir, ia dapat mempertimbangkan modifikasi aktual yang terjadi di jalur kode yang dioptimalkan (termasuk yang reflektif). Jadi itu mungkin masih mengoptimalkan akses secara agresif, tetapi hanya seolah-olah membaca dan menulis masih terjadi dalam urutan program dalam utas pelaksana. Jadi, Anda hanya akan melihat optimasi ketika melihatnya dari utas yang berbeda tanpa konstruksi sinkronisasi yang tepat.

Holger
sumber
tampaknya banyak orang mencoba untuk mengeksploitasi ini final, tetapi, meskipun beberapa telah terbukti menjadi lebih baik, penghematan beberapa nstidak layak melanggar banyak kode lainnya. Alasan mengapa Shenandoah mundur pada beberapa benderanya misalnya
Eugene