Pertanyaan Mengapa membaca yang mudah menguap dan menulis ke anggota lapangan tidak terukur di Jawa?


Amati program berikut yang ditulis di Java (versi lengkap runnable mengikuti, tetapi bagian penting dari program ada di cuplikan sedikit lebih jauh di bawah):

import java.util.ArrayList;



/** A not easy to explain benchmark.
 */
class MultiVolatileJavaExperiment {

    public static void main(String[] args) {
        (new MultiVolatileJavaExperiment()).mainMethod(args);
    }

    int size = Integer.parseInt(System.getProperty("size"));
    int par = Integer.parseInt(System.getProperty("par"));

    public void mainMethod(String[] args) {
        int times = 0;
        if (args.length == 0) times = 1;
        else times = Integer.parseInt(args[0]);
        ArrayList < Long > measurements = new ArrayList < Long > ();

        for (int i = 0; i < times; i++) {
            long start = System.currentTimeMillis();
            run();
            long end = System.currentTimeMillis();

            long time = (end - start);
            System.out.println(i + ") Running time: " + time + " ms");
            measurements.add(time);
        }

        System.out.println(">>>");
        System.out.println(">>> All running times: " + measurements);
        System.out.println(">>>");
    }

    public void run() {
        int sz = size / par;
        ArrayList < Thread > threads = new ArrayList < Thread > ();

        for (int i = 0; i < par; i++) {
            threads.add(new Reader(sz));
            threads.get(i).start();
        }
        for (int i = 0; i < par; i++) {
            try {
                threads.get(i).join();
            } catch (Exception e) {}
        }
    }

    final class Foo {
        int x = 0;
    }

    final class Reader extends Thread {
        volatile Foo vfoo = new Foo();
        Foo bar = null;
        int sz;

        public Reader(int _sz) {
            sz = _sz;
        }

        public void run() {
            int i = 0;
            while (i < sz) {
                vfoo.x = 1;
                // with the following line commented
                // the scalability is almost linear
                bar = vfoo; // <- makes benchmark 2x slower for 2 processors - why?
                i++;
            }
        }
    }

}

Penjelasan: Program ini sebenarnya sangat sederhana. Ini memuat bilangan bulat size dan par dari properti sistem (diteruskan ke jvm dengan -D flag) - ini adalah panjang input dan jumlah utas yang akan digunakan nanti. Ini kemudian mem-parsing argumen baris perintah pertama yang mengatakan berapa banyak waktu untuk mengulang program (kami ingin memastikan bahwa JIT telah melakukan pekerjaannya dan memiliki pengukuran yang lebih andal).

Itu run Metode ini disebut dalam setiap pengulangan. Metode ini dimulai begitu saja par benang, yang masing-masing akan melakukan loop dengan size / par iterasi. Tubuh thread didefinisikan dalam Reader kelas. Setiap pengulangan loop membaca anggota yang mudah menguap vfoo dan menugaskan 1 ke bidang publiknya. Setelah itu, vfoo dibaca sekali lagi dan ditetapkan ke tidak mudah menguap bidang bar.

Perhatikan bagaimana sebagian besar waktu program mengeksekusi badan loop, jadi run di utas adalah fokus tolok ukur ini:

    final class Reader extends Thread {
        volatile Foo vfoo = new Foo();
        Foo bar = null;
        int sz;

        public Reader(int _sz) {
            sz = _sz;
        }

        public void run() {
            int i = 0;
            while (i < sz) {
                vfoo.x = 1;
                // with the following line commented
                // the scalability is almost linear
                bar = vfoo; // <- makes benchmark 2x slower for 2 processors - why?
                i++;
            }
        }
    }

Observasi: Menjalankan java -Xmx512m -Xms512m -server -Dsize=500000000 -Dpar=1 MultiVolatileJavaExperiment 10 pada

Ubuntu Server 10.04.3 LTS
8 core Intel(R) Xeon(R) CPU  X5355  @2.66GHz
~20GB ram
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)

Saya mendapatkan waktu-waktu berikut:

>>> All running times: [821, 750, 1011, 750, 758, 755, 1219, 751, 751, 1012]

Sekarang, pengaturan -Dpar=2, Saya mendapat:

>>> All running times: [1618, 380, 1476, 1245, 1390, 1391, 1445, 1393, 1511, 1508]

Rupanya, ini tidak skala untuk beberapa alasan - saya akan mengharapkan output kedua menjadi dua kali lebih cepat (meskipun tampaknya di salah satu iterasi awal - 380ms).

Menariknya, mengomentari garis bar = vfoo (yang bahkan tidak seharusnya menjadi tulisan yang mudah menguap), menghasilkan waktu-waktu berikut untuk -Dpar mulai 1,2,4,8.

>>> All running times: [762, 563, 563, 563, 563, 563, 570, 566, 563, 563]
>>> All running times: [387, 287, 285, 284, 283, 281, 282, 282, 281, 282]
>>> All running times: [204, 146, 143, 142, 141, 141, 141, 141, 141, 141]
>>> All running times: [120, 78, 74, 74, 81, 75, 73, 73, 72, 71]

Itu skala sempurna.

Analisis: Pertama-tama, tidak ada siklus pengumpulan sampah yang terjadi di sini (saya telah menambahkan -verbose:gc juga untuk memeriksa ini).

Saya mendapatkan hasil yang serupa di iMac saya.

Setiap utas menulis ke bidangnya sendiri, dan berbeda Foo contoh objek milik utas yang berbeda tidak nampak berakhir dalam cachelines yang sama - menambahkan lebih banyak anggota ke dalam Foo untuk meningkatkan ukurannya tidak mengubah ukuran. Setiap contoh objek untaian memiliki lebih dari cukup bidang untuk mengisi baris cache L1. Jadi ini mungkin bukan masalah ingatan.

Pikiran saya berikutnya adalah bahwa JIT mungkin melakukan sesuatu yang aneh, karena iterasi awal biasanya melakukan skala seperti yang diharapkan dalam versi tanpa komentar, jadi saya memeriksanya dengan mencetak rakitan (lihat posting ini tentang cara melakukan itu).

java -Xmx512m -Xms512m -server -XX:CompileCommand=print,*Reader.run MultiVolatileJavaExperiment -Dsize=500000000 -Dpar=1 10

dan saya mendapatkan 2 output ini untuk 2 versi untuk metode Jitted run di Reader. Versi komentar (yang dapat ditingkatkan):

[Verified Entry Point]
  0xf36c9fac: mov    %eax,-0x3000(%esp)
  0xf36c9fb3: push   %ebp
  0xf36c9fb4: sub    $0x8,%esp
  0xf36c9fba: mov    0x68(%ecx),%ebx
  0xf36c9fbd: test   %ebx,%ebx
  0xf36c9fbf: jle    0xf36c9fec
  0xf36c9fc1: xor    %ebx,%ebx
  0xf36c9fc3: nopw   0x0(%eax,%eax,1)
  0xf36c9fcc: xchg   %ax,%ax
  0xf36c9fd0: mov    0x6c(%ecx),%ebp
  0xf36c9fd3: test   %ebp,%ebp
  0xf36c9fd5: je     0xf36c9ff7
  0xf36c9fd7: movl   $0x1,0x8(%ebp)

---------------------------------------------

  0xf36c9fde: mov    0x68(%ecx),%ebp
  0xf36c9fe1: inc    %ebx               ; OopMap{ecx=Oop off=66}
                                        ;*goto
                                        ; - org.scalapool.bench.MultiVolatileJavaExperiment$Reader::run@21 (line 83)

---------------------------------------------

  0xf36c9fe2: test   %edi,0xf7725000    ;   {poll}
  0xf36c9fe8: cmp    %ebp,%ebx
  0xf36c9fea: jl     0xf36c9fd0
  0xf36c9fec: add    $0x8,%esp
  0xf36c9fef: pop    %ebp
  0xf36c9ff0: test   %eax,0xf7725000    ;   {poll_return}
  0xf36c9ff6: ret    
  0xf36c9ff7: mov    $0xfffffff6,%ecx
  0xf36c9ffc: xchg   %ax,%ax
  0xf36c9fff: call   0xf36a56a0         ; OopMap{off=100}
                                        ;*putfield x
                                        ; - org.scalapool.bench.MultiVolatileJavaExperiment$Reader::run@15 (line 79)
                                        ;   {runtime_call}
  0xf36ca004: call   0xf6f877a0         ;   {runtime_call}

Tanpa tanda komentar bar = vfoo (non-scalable, lebih lambat) versi:

[Verified Entry Point]
  0xf3771aac: mov    %eax,-0x3000(%esp)
  0xf3771ab3: push   %ebp
  0xf3771ab4: sub    $0x8,%esp
  0xf3771aba: mov    0x68(%ecx),%ebx
  0xf3771abd: test   %ebx,%ebx
  0xf3771abf: jle    0xf3771afe
  0xf3771ac1: xor    %ebx,%ebx
  0xf3771ac3: nopw   0x0(%eax,%eax,1)
  0xf3771acc: xchg   %ax,%ax
  0xf3771ad0: mov    0x6c(%ecx),%ebp
  0xf3771ad3: test   %ebp,%ebp
  0xf3771ad5: je     0xf3771b09
  0xf3771ad7: movl   $0x1,0x8(%ebp)

-------------------------------------------------

  0xf3771ade: mov    0x6c(%ecx),%ebp
  0xf3771ae1: mov    %ebp,0x70(%ecx)
  0xf3771ae4: mov    0x68(%ecx),%edi
  0xf3771ae7: inc    %ebx
  0xf3771ae8: mov    %ecx,%eax
  0xf3771aea: shr    $0x9,%eax
  0xf3771aed: movb   $0x0,-0x3113c300(%eax)  ; OopMap{ecx=Oop off=84}
                                        ;*goto
                                        ; - org.scalapool.bench.MultiVolatileJavaExperiment$Reader::run@29 (line 83)

-----------------------------------------------

  0xf3771af4: test   %edi,0xf77ce000    ;   {poll}
  0xf3771afa: cmp    %edi,%ebx
  0xf3771afc: jl     0xf3771ad0
  0xf3771afe: add    $0x8,%esp
  0xf3771b01: pop    %ebp
  0xf3771b02: test   %eax,0xf77ce000    ;   {poll_return}
  0xf3771b08: ret    
  0xf3771b09: mov    $0xfffffff6,%ecx
  0xf3771b0e: nop    
  0xf3771b0f: call   0xf374e6a0         ; OopMap{off=116}
                                        ;*putfield x
                                        ; - org.scalapool.bench.MultiVolatileJavaExperiment$Reader::run@15 (line 79)
                                        ;   {runtime_call}
  0xf3771b14: call   0xf70307a0         ;   {runtime_call}

Perbedaan dalam kedua versi ada di dalam ---------. Saya mengharapkan untuk menemukan instruksi sinkronisasi dalam perakitan yang mungkin menjelaskan masalah kinerja - sementara beberapa tambahan shift, mov dan inc petunjuk mungkin memengaruhi angka kinerja absolut, saya tidak melihat bagaimana pengaruhnya terhadap skalabilitas.

Jadi, saya menduga bahwa ini adalah semacam masalah memori yang berkaitan dengan penyimpanan ke lapangan di kelas. Di sisi lain, saya juga cenderung percaya bahwa JIT melakukan sesuatu yang lucu, karena dalam satu iterasi waktu yang terukur aku s dua kali lebih cepat, sebagaimana seharusnya.

Adakah yang bisa menjelaskan apa yang terjadi di sini? Harap tepat dan sertakan referensi yang mendukung klaim Anda.

Terima kasih!

EDIT:

Berikut ini kode byte untuk versi cepat (skalabel):

public void run();
  LineNumberTable: 
   line 77: 0
   line 78: 2
   line 79: 10
   line 83: 18
   line 85: 24



  Code:
   Stack=2, Locals=2, Args_size=1
   0:   iconst_0
   1:   istore_1
   2:   iload_1
   3:   aload_0
   4:   getfield    #7; //Field sz:I
   7:   if_icmpge   24
   10:  aload_0
   11:  getfield    #5; //Field vfoo:Lorg/scalapool/bench/MultiVolatileJavaExperiment$Foo;
   14:  iconst_1
   15:  putfield    #8; //Field org/scalapool/bench/MultiVolatileJavaExperiment$Foo.x:I
   18:  iinc    1, 1
   21:  goto    2
   24:  return
  LineNumberTable: 
   line 77: 0
   line 78: 2
   line 79: 10
   line 83: 18
   line 85: 24

  StackMapTable: number_of_entries = 2
   frame_type = 252 /* append */
     offset_delta = 2
     locals = [ int ]
   frame_type = 21 /* same */

Versi lambat (tidak dapat diskalakan) dengan bar = vfoo:

public void run();
  LineNumberTable: 
   line 77: 0
   line 78: 2
   line 79: 10
   line 82: 18
   line 83: 26
   line 85: 32



  Code:
   Stack=2, Locals=2, Args_size=1
   0:   iconst_0
   1:   istore_1
   2:   iload_1
   3:   aload_0
   4:   getfield    #7; //Field sz:I
   7:   if_icmpge   32
   10:  aload_0
   11:  getfield    #5; //Field vfoo:Lorg/scalapool/bench/MultiVolatileJavaExperiment$Foo;
   14:  iconst_1
   15:  putfield    #8; //Field org/scalapool/bench/MultiVolatileJavaExperiment$Foo.x:I
   18:  aload_0
   19:  aload_0
   20:  getfield    #5; //Field vfoo:Lorg/scalapool/bench/MultiVolatileJavaExperiment$Foo;
   23:  putfield    #6; //Field bar:Lorg/scalapool/bench/MultiVolatileJavaExperiment$Foo;
   26:  iinc    1, 1
   29:  goto    2
   32:  return
  LineNumberTable: 
   line 77: 0
   line 78: 2
   line 79: 10
   line 82: 18
   line 83: 26
   line 85: 32

  StackMapTable: number_of_entries = 2
   frame_type = 252 /* append */
     offset_delta = 2
     locals = [ int ]
   frame_type = 29 /* same */

Semakin saya bereksperimen dengan ini, tampaknya bagi saya ini tidak ada hubungannya dengan volatil sama sekali - itu ada hubungannya dengan menulis ke bidang objek. Firasat saya adalah bahwa ini entah bagaimana masalah contention memori - sesuatu dengan cache dan berbagi palsu, meskipun tidak ada sinkronisasi eksplisit sama sekali.

EDIT 2:

Menariknya, mengubah program seperti ini:

final class Holder {
    public Foo bar = null;
}

final class Reader extends Thread {
    volatile Foo vfoo = new Foo();
    Holder holder = null;
    int sz;

    public Reader(int _sz) {
        sz = _sz;
    }

    public void run() {
        int i = 0;
        holder = new Holder();
        while (i < sz) {
            vfoo.x = 1;
            holder.bar = vfoo;
            i++;
        }
    }
}

menyelesaikan masalah penskalaan. Ternyata, itu Holder objek di atas dibuat setelah utas dimulai, dan mungkin dialokasikan dalam segmen memori yang berbeda, yang kemudian dimodifikasi secara bersamaan, dibandingkan dengan memodifikasi bidang bar di objek utas, yang entah bagaimana "dekat" dalam memori di antara beberapa utas benang.


32
2018-01-19 21:43


asal


Jawaban:


Inilah yang saya pikir sedang terjadi (perlu diingat saya tidak akrab dengan HotSpot):

0xf36c9fd0: mov    0x6c(%ecx),%ebp    ; vfoo
0xf36c9fd3: test   %ebp,%ebp          ; vfoo is null?
0xf36c9fd5: je     0xf36c9ff7         ;   throw NullPointerException (I guess)
0xf36c9fd7: movl   $0x1,0x8(%ebp)     ; vfoo.x = 1
0xf36c9fde: mov    0x68(%ecx),%ebp    ; sz
0xf36c9fe1: inc    %ebx               ; i++
0xf36c9fe2: test   %edi,0xf7725000    ; safepoint on end of loop
0xf36c9fe8: cmp    %ebp,%ebx          ; i < sz?
0xf36c9fea: jl     0xf36c9fd0


0xf3771ad0: mov    0x6c(%ecx),%ebp          ; vfoo
0xf3771ad3: test   %ebp,%ebp                ; vfoo is null?
0xf3771ad5: je     0xf3771b09               ;   throw NullPointerException (I guess)
0xf3771ad7: movl   $0x1,0x8(%ebp)           ; vfoo.x = 1
0xf3771ade: mov    0x6c(%ecx),%ebp          ; \
0xf3771ae1: mov    %ebp,0x70(%ecx)          ; / bar = vfoo
0xf3771ae4: mov    0x68(%ecx),%edi          ; sz
0xf3771ae7: inc    %ebx                     ; i++
0xf3771ae8: mov    %ecx,%eax                ; 
0xf3771aea: shr    $0x9,%eax                ; ??? \ Probably replaced later
0xf3771aed: movb   $0x0,-0x3113c300(%eax)   ; ??? / by some barrier code?
0xf3771af4: test   %edi,0xf77ce000          ; safepoint
0xf3771afa: cmp    %edi,%ebx                ; i < sz ?
0xf3771afc: jl     0xf3771ad0               ;

Alasan saya pikir kode di atas adalah penghalang adalah ketika mengambil NullPointerException, versi skalabel memiliki XCHG, yang berfungsi sebagai penghalang, sedangkan versi non-skalabel memiliki NOP di sana.

Dasar pemikirannya adalah bahwa perlu terjadi sebelum pemesanan antara pemuatan awal vfoo dan bergabung dengan utas. Dalam kasus volatile, penghalang akan berada di dalam loop, jadi tidak perlu berada di tempat lain. Apa yang saya tidak mengerti adalah mengapa XCHG tidak digunakan di dalam loop. Mungkin deteksi runtime dari dukungan MFENCE?


3
2018-01-19 13:40



Mari kita coba JVM berperilaku sedikit lebih "konsisten." Compiler JIT benar-benar membuang perbandingan uji coba; jadi ayo nonaktifkan compiler JIT dengan menggunakan -Djava.compiler=NONE. Ini benar-benar memperkenalkan kinerja hit, tetapi akan membantu menghilangkan ketidakjelasan dan efek dari pengoptimalan kompilator JIT.

Pengumpulan sampah memperkenalkan kerumitannya sendiri. Mari gunakan pengumpul sampah serial dengan menggunakan -XX:+UseSerialGC. Mari juga nonaktifkan koleksi sampah eksplisit dan aktifkan beberapa penebangan untuk melihat kapan pengumpulan sampah dilakukan: -verbose:gc -XX:+DisableExplicitGC. Akhirnya, mari kita mengumpulkan cukup banyak alokasi -Xmx128m -Xms128m.

Sekarang kita dapat menjalankan tes menggunakan:

java -XX:+UseSerialGC -verbose:gc -XX:+DisableExplicitGC -Djava.compiler=NONE -Xmx128m -Xms128m -server -Dsize=50000000 -Dpar=1 MultiVolatileJavaExperiment 10

Menjalankan pengujian berkali-kali menunjukkan hasilnya sangat konsisten (saya menggunakan Oracle Java 1.6.0_24-b07 pada Ubuntu 10.04.3 LTS dengan Intel (R) Core (TM) 2 Duo CPU P8700 @ 2.53GHz), rata-rata di suatu tempat sekitar 2050 milidetik. Jika saya berkomentar bar = vfoobaris, saya secara konsisten rata-rata sekitar 1280 milidetik. Menjalankan tes menggunakan -Dpar=2 hasil dengan rata-rata sekitar 1.350 milidetik dengan bar = vfoo dan sekitar 1005 milidetik dengan itu berkomentar.

+=========+======+=========+
| Threads | With | Without |
+=========+======+=========+
|    1    | 2050 |  1280   |
+---------+------+---------+
|    2    | 1350 |  1005   |
+=========+======+=========+

Mari sekarang lihat kode dan lihat apakah kita dapat melihat alasan mengapa multi-threading tidak efisien. Di Reader.run(), variabel kualifikasi dengan this yang sesuai akan membantu membuatnya jelas variabel mana yang bersifat lokal:

int i = 0;
while (i < this.sz) {
    this.vfoo.x = 1;
    this.bar = this.vfoo;
    i++;
}

Hal pertama yang harus diperhatikan adalah while loop berisi empat variabel yang direferensikan this. Ini berarti kode mengakses kolam konstanta runtime kelas dan melakukan pengecekan jenis (melalui getfield instruksi bytecode). Mari kita ubah kode untuk mencoba dan menghilangkan akses runtime pool konstan dan lihat apakah kita mendapatkan manfaat apa pun.

final int mysz = this.sz;
int i = 0;
while (i < mysz) {
    this.vfoo.x = 1;
    this.bar = this.vfoo;
    i++;
}

Di sini, kami menggunakan lokal mysz variabel untuk mengakses ukuran lingkaran dan hanya mengakses sz melalui this satu kali, untuk inisialisasi. Menjalankan tes, dengan dua benang, rata-rata sekitar 1295 milidetik; manfaat kecil, tetapi satu saja.

Melihat ke while loop, apakah kita benar-benar perlu referensi this.vfoo dua kali? Kedua pembacaan yang mudah menguap menciptakan dua tepi sinkronisasi yang mesin virtual (dan perangkat keras yang mendasari, dalam hal ini) perlu dikelola. Katakanlah kita menginginkan satu tepi sinkronisasi di awal while loop dan kita tidak butuh dua, kita bisa menggunakan yang berikut:

final int mysz = this.sz;
Foo myvfoo = null;
int i = 0;
while (i < mysz) {
    myvfoo = this.vfoo;
    myvfoo.x = 1;
    this.bar = myvfoo;
    i++;
}

Ini rata-rata sekitar 1122 milidetik; masih menjadi lebih baik. Bagaimana dengan itu this.bar referensi? Karena kita berbicara multi-threading, katakanlah perhitungan dalam while loop adalah apa yang kita ingin mendapatkan manfaat multi-thread dari dan this.bar adalah bagaimana kami mengkomunikasikan hasil kami kepada orang lain. Kami benar-benar tidak ingin mengatur this.bar sampai setelah itu while loop selesai.

final int mysz = this.sz;
Foo myvfoo = null;
Foo mybar = null;
int i = 0;
while (i < mysz) {
    myvfoo = this.vfoo;
    myvfoo.x = 1;
    mybar = myvfoo;
    i++;
}
this.bar = mybar;

Yang memberi kita sekitar 857 milidetik rata-rata. Masih ada yang final this.vfoo referensi dalam while lingkaran. Asumsikan lagi bahwa while loop adalah apa yang kita inginkan manfaat multi-thread dari, mari kita pindahkan itu this.vfoo diluar while lingkaran.

final int mysz = this.sz;
final Foo myvfoo = this.vfoo;
Foo mybar = null;
int i = 0;
while (i < mysz) {
    myvfoo.x = 1;
    mybar = myvfoo;
    i++;
}
final Foo vfoocheck = this.vfoo;
if (vfoocheck != myvfoo) {
    System.out.println("vfoo changed from " + myvfoo + " to " + vfoocheck);
}
this.bar = mybar;

Sekarang kami rata-rata sekitar 502 milidetik; tes single-threaded rata-rata sekitar 900 milidetik.

jadi apa maksud dari ini? Dengan mengekstrapolasi referensi variabel non-lokal dari while loop, ada manfaat kinerja yang signifikan baik dalam tes single-dan double-threaded. Versi asli dari MultiVolatileJavaExperiment mengukur biaya akses non-lokal variabel 50.000.000 kali, sedangkan versi terakhir mengukur biaya pengaksesan lokal variabel 50.000.000 kali. Dengan menggunakan variabel lokal, Anda meningkatkan kemungkinan bahwa Java Virtual Machine dan perangkat keras yang mendasarinya dapat mengelola cache thread dengan lebih efisien.

Akhirnya, mari kita jalankan tes yang biasanya digunakan (perhatikan, menggunakan ukuran pengulangan 500.000.000 bukannya 50.000.000):

java -Xmx128m -Xms128m -server -Dsize=500000000 -Dpar=2 MultiVolatileJavaExperiment 10

Versi asli rata-rata sekitar 1100 milidetik dan versi yang dimodifikasi rata-rata sekitar 10 milidetik.


3
2018-01-19 13:56



Anda sebenarnya tidak menulis ke bidang yang mudah menguap sehingga bidang yang mudah menguap dapat di-cache di setiap utas.

Menggunakan volatile mencegah beberapa optimisasi kompilator dan dalam tolok ukur mikro, Anda dapat melihat perbedaan relatif yang besar.

Dalam contoh di atas, versi yang dikomentari lebih panjang karena memiliki loop yang tidak dikendalikan untuk menempatkan dua iterasi dalam satu lingkaran aktual. Ini hampir bisa menggandakan kinerja.

Ketika menggunakan volatile Anda dapat melihat tidak ada loop yang membuka gulungan.

BTW: Anda dapat menghapus banyak kode dalam contoh Anda untuk membuatnya lebih mudah dibaca. ;)


2



Edit: Jawaban ini tidak tahan terhadap pengujian.

Saya tidak memiliki cara untuk menguji ini sekarang (tidak ada multicore CPU di mesin ini), tetapi di sini adalah teori: The Foo instance mungkin tidak berada di baris cache yang sama, tetapi mungkin Reader contoh adalah.

Ini berarti perlambatan dapat dijelaskan dengan menulis ke bar, daripada membaca foo, karena menulis kepada bar akan membatalkan baris cache untuk inti lainnya dan menyebabkan banyak penyalinan di antara cache. Mengomentari penulisan ke bar (Yang hanya menulis ke bidang Reader dalam loop) menghentikan perlambatan, yang konsisten dengan penjelasan ini.

Edit: Menurut artikel ini, tata letak memori benda sedemikian rupa sehingga bar referensi akan menjadi bidang terakhir dalam tata letak Reader obyek. Ini berarti kemungkinan untuk mendarat di baris cache yang sama sebagai objek berikutnya di Heap. Karena saya tidak yakin tentang urutan di mana objek baru dialokasikan pada Heap, saya menyarankan di komentar di bawah ini untuk memasukkan kedua jenis objek "panas" dengan referensi, yang akan efektif dalam memisahkan objek (Setidaknya, saya harap akan, tetapi itu tergantung pada bagaimana bidang dari jenis yang sama diurutkan dalam memori).


1



Singkat: tampaknya, jawabannya adalah pembagian yang salah karena menandai kartu untuk GC.

Penjelasan lebih luas diberikan dalam pertanyaan ini:

Array alokasi dan akses pada Java Virtual Machine dan pertentangan memori


1