Apakah volatile mahal?


111

Setelah membaca The JSR-133 Cookbook for Compiler Writers tentang implementasi volatile, terutama bagian "Interactions with Atomic Instructions" Saya berasumsi bahwa membaca variabel volatile tanpa memperbaruinya membutuhkan LoadLoad atau penghalang LoadStore. Lebih jauh ke bawah halaman saya melihat bahwa LoadLoad dan LoadStore secara efektif tidak ada operasi pada CPU X86. Apakah ini berarti bahwa operasi pembacaan volatile dapat dilakukan tanpa pembatalan cache eksplisit pada x86, dan secepat pembacaan variabel normal (dengan mengabaikan batasan pengurutan ulang volatile)?

Saya yakin saya tidak memahami ini dengan benar. Bisakah seseorang peduli untuk mencerahkan saya?

EDIT: Saya ingin tahu apakah ada perbedaan dalam lingkungan multi-prosesor. Pada sistem CPU tunggal, CPU mungkin melihat cache utasnya sendiri, seperti yang dinyatakan oleh John V., tetapi pada sistem multi CPU harus ada beberapa opsi konfigurasi ke CPU bahwa ini tidak cukup dan memori utama harus dipukul, membuat volatile lebih lambat pada sistem multi cpu, bukan?

PS: Dalam perjalanan saya untuk mempelajari lebih lanjut tentang ini, saya tersandung tentang artikel bagus berikut, dan karena pertanyaan ini mungkin menarik bagi orang lain, saya akan membagikan tautan saya di sini:


1
Anda dapat membaca suntingan saya tentang konfigurasi dengan beberapa CPU yang Anda maksud. Hal ini dapat terjadi bahwa pada sistem multi CPU untuk referensi berumur pendek, tidak lebih dari satu membaca / menulis ke memori utama akan terjadi.
John Vint

2
volatile read itu sendiri tidak mahal. biaya utamanya adalah bagaimana cara mencegah pengoptimalan. dalam praktiknya biaya rata-rata juga tidak terlalu tinggi, kecuali volatile digunakan dalam loop yang ketat.
bereputasi

2
Artikel tentang infoq ini ( infoq.com/articles/memory_barriers_jvm_concurrency ) mungkin juga menarik bagi Anda, ini menunjukkan efek volatile dan disinkronkan pada kode yang dihasilkan untuk arsitektur yang berbeda. Ini juga merupakan salah satu kasus di mana jvm dapat bekerja lebih baik daripada kompiler sebelumnya, karena ia tahu apakah ia berjalan pada sistem uniprosesor dan dapat menghilangkan beberapa hambatan memori.
Jörn Horstmann

Jawaban:


123

Di Intel, pembacaan volatile yang tidak bisa dibantah cukup murah. Jika kita mempertimbangkan kasus sederhana berikut:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

Menggunakan kemampuan Java 7 untuk mencetak kode assembly, metode run terlihat seperti ini:

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

Jika Anda melihat 2 referensi untuk getstatic, yang pertama melibatkan beban dari memori, yang kedua melewatkan beban karena nilainya digunakan kembali dari register yang sudah dimuat ke dalamnya (panjangnya 64 bit dan di laptop 32 bit saya itu menggunakan 2 register).

Jika kita membuat variabel l mudah menguap, perakitan yang dihasilkan berbeda.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

Dalam hal ini, kedua referensi getstatic ke variabel l melibatkan beban dari memori, yaitu nilai tidak dapat disimpan dalam register di beberapa pembacaan volatil. Untuk memastikan bahwa ada atomic membaca nilai dibaca dari memori utama ke dalam register MMX movsd 0x6fb7b2f0(%ebp),%xmm0membuat operasi baca instruksi tunggal (dari contoh sebelumnya kita melihat bahwa nilai 64bit biasanya membutuhkan dua 32bit membaca pada sistem 32bit).

Jadi biaya keseluruhan dari pembacaan volatil kira-kira akan setara dengan beban memori dan bisa semurah akses cache L1. Namun jika inti lain menulis ke variabel volatile, baris cache akan menjadi tidak valid sehingga membutuhkan memori utama atau mungkin akses cache L3. Biaya sebenarnya akan sangat bergantung pada arsitektur CPU. Bahkan antara Intel dan AMD, protokol koherensi cache berbeda.


catatan samping, java 6 memiliki kemampuan yang sama untuk menunjukkan perakitan (hotspot yang melakukannya)
bestsss

1 Di JDK5 dapat mudah menguap tidak mengatur kembali sehubungan dengan setiap baca / tulis (yang perbaikan penguncian ganda cek, misalnya). Apakah itu menyiratkan bahwa itu juga akan memengaruhi bagaimana bidang non-volatile dimanipulasi? Akan menarik untuk menggabungkan akses ke bidang yang mudah menguap dan tidak mudah menguap.
ewernli

@evemli, Anda harus berhati-hati, saya pernah membuat pernyataan ini sendiri, tetapi ternyata tidak benar. Ada kasus tepi. Model Memori Java memungkinkan semantik motel kecoak, saat penyimpanan dapat dipesan ulang sebelum penyimpanan yang mudah menguap. Jika Anda mengambil ini dari artikel Brian Goetz di situs IBM, maka perlu disebutkan bahwa artikel ini terlalu menyederhanakan spesifikasi JMM.
Michael Barker

20

Secara umum, pada kebanyakan prosesor modern, beban yang mudah menguap sebanding dengan beban normal. Sebuah penyimpanan yang mudah menguap sekitar 1/3 waktu montior-enter / monitor-exit. Ini terlihat pada sistem yang koheren cache.

Untuk menjawab pertanyaan OP, penulisan volatile mahal sedangkan read biasanya tidak.

Apakah ini berarti bahwa operasi pembacaan volatile dapat dilakukan tanpa pembatalan cache eksplisit pada x86, dan secepat pembacaan variabel normal (dengan mengabaikan batasan pengurutan ulang volatile)?

Ya, terkadang saat memvalidasi bidang, CPU bahkan mungkin tidak mengenai memori utama, sebagai gantinya memata-matai cache utas lain dan mendapatkan nilainya dari sana (penjelasan yang sangat umum).

Namun, saya mendukung saran Neil bahwa jika Anda memiliki bidang yang diakses oleh beberapa utas, Anda harus membungkusnya sebagai AtomicReference. Sebagai AtomicReference, ia mengeksekusi throughput yang kira-kira sama untuk baca / tulis, tetapi juga lebih jelas bahwa bidang akan diakses dan dimodifikasi oleh banyak utas.

Edit untuk menjawab edit OP:

Koherensi cache adalah protokol yang sedikit rumit, tetapi singkatnya: CPU akan berbagi baris cache umum yang terpasang ke memori utama. Jika CPU memuat memori dan tidak ada CPU lain yang memilikinya, CPU akan menganggapnya sebagai nilai yang paling mutakhir. Jika CPU lain mencoba memuat lokasi memori yang sama, CPU yang sudah dimuat akan mengetahui hal ini dan benar-benar membagikan referensi yang di-cache ke CPU yang meminta - sekarang CPU permintaan memiliki salinan dari memori itu di cache CPU-nya. (Tidak pernah harus mencari di memori utama untuk referensi)

Ada lebih banyak protokol yang terlibat tetapi ini memberi gambaran tentang apa yang sedang terjadi. Juga untuk menjawab pertanyaan Anda yang lain, dengan tidak adanya banyak prosesor, baca / tulis volatil sebenarnya bisa lebih cepat daripada dengan banyak prosesor. Ada beberapa aplikasi yang sebenarnya akan berjalan lebih cepat secara bersamaan dengan satu CPU kemudian beberapa.


5
AtomicReference hanyalah pembungkus untuk bidang volatile dengan menambahkan fungsi asli yang menyediakan fungsionalitas tambahan seperti getAndSet, bandingkanAndSet, dll., Jadi dari sudut pandang kinerja yang menggunakannya hanya berguna jika Anda membutuhkan fungsionalitas tambahan. Tapi saya heran mengapa Anda merujuk ke OS di sini? Fungsionalitas tersebut diimplementasikan dalam opcode CPU secara langsung. Dan apakah ini menyiratkan bahwa pada sistem multi prosesor, di mana satu CPU tidak memiliki pengetahuan tentang isi cache dari CPU lain yang volatilnya lebih lambat karena CPU selalu harus mencapai memori utama?
Daniel

Anda benar, saya rindu berbicara tentang OS yang seharusnya menulis CPU, memperbaikinya sekarang. Dan ya, saya tahu AtomicReference hanyalah pembungkus untuk bidang yang mudah menguap tetapi juga menambahkan sebagai semacam dokumentasi bahwa bidang itu sendiri akan diakses oleh banyak utas.
John Vint

@ John, mengapa Anda menambahkan pengalihan lain melalui AtomicReference? Jika Anda membutuhkan CAS - ok, tetapi AtomicUpdater bisa menjadi pilihan yang lebih baik. Sejauh yang saya ingat tidak ada intrinsik tentang AtomicReference.
bestsss

@bestsss Untuk semua tujuan umum, Anda benar tidak ada perbedaan antara AtomicReference.set / get dan beban volatil dan penyimpanan. Itu dikatakan saya memiliki perasaan yang sama (dan lakukan sampai taraf tertentu) tentang kapan harus menggunakan yang mana. Tanggapan ini dapat merinci sedikit stackoverflow.com/questions/3964317/… . Menggunakan salah satu lebih merupakan preferensi, satu-satunya argumen saya untuk menggunakan AtomicReference melalui volatile sederhana adalah untuk dokumentasi yang jelas - itu sendiri juga tidak membuat argumen terbesar yang saya mengerti
John Vint

Di samping catatan, beberapa orang berpendapat menggunakan kolom volatile / AtomicReference (tanpa perlu CAS) mengarah ke kode buggy old.nabble.com/…
John Vint

12

Dalam kata-kata Model Memori Java (seperti yang didefinisikan untuk Java 5+ di JSR 133), setiap operasi - baca atau tulis - pada volatilevariabel membuat hubungan terjadi-sebelum sehubungan dengan operasi lain pada variabel yang sama. Ini berarti bahwa compiler dan JIT dipaksa untuk menghindari pengoptimalan tertentu seperti menata ulang instruksi dalam utas atau melakukan operasi hanya dalam cache lokal.

Karena beberapa pengoptimalan tidak tersedia, kode yang dihasilkan harus lebih lambat dari sebelumnya, meskipun mungkin tidak terlalu banyak.

Namun demikian, Anda tidak boleh membuat variabel volatilekecuali Anda tahu bahwa itu akan diakses dari beberapa utas di luar synchronizedblok. Bahkan kemudian Anda harus mempertimbangkan apakah volatile adalah pilihan terbaik versus synchronized, AtomicReferencedan temannya, Lockkelas eksplisit , dll.


4

Mengakses variabel volatil dalam banyak hal mirip dengan membungkus akses ke variabel biasa dalam blok tersinkronisasi. Misalnya, akses ke variabel volatile mencegah CPU memesan ulang instruksi sebelum dan sesudah akses, dan ini biasanya memperlambat eksekusi (meskipun saya tidak bisa mengatakan seberapa banyak).

Secara lebih umum, pada sistem multi-prosesor, saya tidak melihat bagaimana akses ke variabel volatil dapat dilakukan tanpa penalti - harus ada beberapa cara untuk memastikan penulisan pada prosesor A akan disinkronkan dengan pembacaan pada prosesor B.


4
Membaca variabel volatil memiliki hukuman yang sama daripada melakukan monitor-enter, berkenaan dengan kemungkinan penyusunan ulang instruksi, sementara menulis variabel volatil sama dengan keluar monitor. Perbedaan mungkin adalah variabel mana (misalnya cache prosesor) yang dibilas atau tidak valid. Saat sinkronisasi menghapus atau membatalkan semuanya, akses ke variabel volatil harus selalu mengabaikan cache.
Daniel

12
-1, Mengakses variabel yang mudah menguap sedikit berbeda dengan menggunakan blok tersinkronisasi. Memasuki blok tersinkronisasi memerlukan penulisan berbasis atomic CompareAndSet untuk mengeluarkan kunci dan menulis volatile untuk melepaskannya. Jika kunci puas maka kontrol harus berpindah dari ruang pengguna ke ruang kernel untuk mengatur kunci (ini adalah bit yang mahal). Mengakses volatile akan selalu berada di ruang pengguna.
Michael Barker

@MichaelBarker: Apakah Anda yakin bahwa semua monitor harus dijaga oleh kernel dan bukan aplikasinya?
Daniel

@Daniel: Jika Anda mewakili monitor menggunakan blok tersinkronisasi atau Lock maka ya, tetapi hanya jika monitor puas. Satu-satunya cara untuk melakukan ini tanpa arbitrase kernel adalah dengan menggunakan logika yang sama, tetapi sibuk berputar alih-alih memarkir utas.
Michael Barker

@MichaelBarker: Oke, untuk gembok puas saya mengerti ini.
Daniel
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.