Dalam bahasa tingkat rendah (C, C ++ atau apa pun): Saya memiliki pilihan di antara apakah memiliki banyak mutex (seperti apa yang diberikan pthread kepada saya atau apa pun yang disediakan pustaka sistem asli) atau yang tunggal untuk objek.
Seberapa efisien untuk mengunci mutex? Yaitu berapa banyak instruksi assembler yang ada dan berapa banyak waktu yang diperlukan (dalam hal mutex tidak terkunci)?
Berapa biaya mutex? Apakah benar ada banyak mutex yang bermasalah ? Atau bisakah saya membuang variabel mutex dalam kode saya karena saya memiliki int
variabel dan itu tidak terlalu penting?
(Saya tidak yakin berapa banyak perbedaan antara perangkat keras yang berbeda. Jika ada, saya juga ingin tahu tentang mereka. Tetapi kebanyakan, saya tertarik dengan perangkat keras umum.)
Intinya adalah, dengan menggunakan banyak mutex yang masing-masing hanya mencakup sebagian dari objek dan bukan satu mutex untuk seluruh objek, saya bisa mengamankan banyak blok. Dan saya bertanya-tanya seberapa jauh saya harus melakukan ini. Ie haruskah saya mencoba untuk mengamankan blok yang mungkin sejauh mungkin, tidak peduli seberapa rumit dan berapa banyak mutexes artinya?
Posting blog WebKits (2016) tentang penguncian sangat terkait dengan pertanyaan ini, dan menjelaskan perbedaan antara spinlock, kunci adaptif, futex, dll.
sumber
Jawaban:
Jika Anda memiliki banyak utas dan akses ke objek sering terjadi, beberapa kunci akan meningkatkan paralelisme. Dengan biaya pemeliharaan, karena lebih banyak penguncian berarti lebih banyak debug dari penguncian.
Instruksi assembler yang tepat adalah overhead terkecil dari mutex - jaminan koherensi memori / cache adalah overhead utama. Dan jarang kunci khusus diambil - lebih baik.
Mutex terbuat dari dua bagian utama (penyederhanaan berlebihan): (1) sebuah bendera yang menunjukkan apakah mutex terkunci atau tidak dan (2) menunggu antrian.
Pergantian bendera hanya beberapa instruksi dan biasanya dilakukan tanpa panggilan sistem. Jika mutex terkunci, syscall akan terjadi untuk menambahkan utas panggilan ke antrian tunggu dan mulai menunggu. Membuka kunci, jika antrian tunggu kosong, murah tetapi sebaliknya memerlukan syscall untuk membangunkan salah satu proses menunggu. (Pada beberapa sistem syscalls murah / cepat digunakan untuk mengimplementasikan mutex, mereka menjadi lambat (normal) panggilan sistem hanya dalam kasus pertengkaran.)
Mengunci mutex yang tidak terkunci benar-benar murah. Membuka kunci mutex tanpa pertengkaran juga murah.
Anda dapat membuang variabel mutex ke dalam kode sesuai keinginan. Anda hanya dibatasi oleh jumlah memori yang dapat dialokasikan aplikasi Anda.
Ringkasan. Kunci ruang pengguna (dan mutex khususnya) murah dan tidak dikenakan batas sistem apa pun. Tetapi terlalu banyak dari mereka mengeja mimpi buruk untuk debugging. Meja sederhana:
Skema penguncian yang seimbang untuk aplikasi harus ditemukan dan dipelihara, umumnya menyeimbangkan # 2 dan # 3.
(*) Masalah dengan mutasi yang lebih jarang terkunci adalah bahwa jika Anda memiliki terlalu banyak penguncian dalam aplikasi Anda, itu menyebabkan banyak lalu lintas antar-CPU / inti untuk menyiram memori mutex dari cache data dari CPU lain untuk menjamin koherensi cache. Cache flushes seperti interupsi ringan dan ditangani oleh CPU secara transparan - tetapi mereka memperkenalkan warung yang disebut (mencari "stall").
Dan warung inilah yang membuat kode penguncian berjalan lambat, seringkali tanpa indikasi yang jelas mengapa aplikasi lambat. (Beberapa lengkungan menyediakan statistik lalu lintas antar-CPU / inti, beberapa tidak.)
Untuk menghindari masalah, orang biasanya menggunakan sejumlah besar kunci untuk mengurangi kemungkinan pertikaian kunci dan untuk menghindari kios. Itulah alasan mengapa penguncian ruang pengguna yang murah, tidak dikenakan batas sistem, ada.
sumber
Saya ingin tahu hal yang sama, jadi saya mengukurnya. Di komputer saya (AMD FX (tm) -8150 Prosesor Delapan-Inti pada 3,612361 GHz), mengunci dan membuka kunci sebuah mutex yang tidak terkunci yang berada dalam jalur cache sendiri dan sudah di-cache, membutuhkan 47 jam (13 ns).
Karena sinkronisasi antara dua inti (saya menggunakan CPU # 0 dan # 1), saya hanya bisa memanggil pasangan kunci / buka sekali setiap 102 n pada dua utas, jadi sekali setiap 51 n, dari mana orang dapat menyimpulkan bahwa dibutuhkan sekitar 38 untuk memulihkan setelah utas melakukan pembukaan kunci sebelum utas berikutnya dapat menguncinya lagi.
Program yang saya gunakan untuk menyelidiki ini dapat ditemukan di sini: https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx
Perhatikan bahwa ia memiliki beberapa nilai hardcoded khusus untuk kotak saya (xrange, yrange dan overhead rdtsc), jadi Anda mungkin harus bereksperimen dengannya sebelum itu bekerja untuk Anda.
Grafik yang dihasilkannya dalam keadaan itu adalah:
Ini menunjukkan hasil benchmark berjalan pada kode berikut:
Dua panggilan rdtsc mengukur jumlah jam yang diperlukan untuk mengunci dan membuka `mutex '(dengan overhead 39 jam untuk panggilan rdtsc di kotak saya). ASM ketiga adalah loop penundaan. Ukuran loop tunda 1 hitungan lebih kecil untuk utas 1 daripada utas 0, jadi utas 1 sedikit lebih cepat.
Fungsi di atas disebut dalam loop ketat ukuran 100.000. Meskipun demikian fungsinya sedikit lebih cepat untuk utas 1, kedua loop menyinkronkan karena panggilan ke mutex. Ini terlihat dalam grafik dari fakta bahwa jumlah jam yang diukur untuk pasangan kunci / buka sedikit lebih besar untuk ulir 1, untuk memperhitungkan keterlambatan yang lebih pendek dalam loop di bawahnya.
Dalam grafik di atas, titik kanan bawah adalah pengukuran dengan loop_count penundaan 150, dan kemudian mengikuti titik-titik di bawah, ke kiri, loop_count dikurangi dengan satu pengukuran masing-masing. Ketika menjadi 77 fungsi dipanggil setiap 102 ns di kedua utas. Jika kemudian loop_count dikurangi lebih jauh, maka tidak mungkin lagi mensinkronkan utas dan mutex mulai benar-benar terkunci sebagian besar waktu, menghasilkan peningkatan jumlah jam yang diperlukan untuk melakukan kunci / membuka kunci. Juga waktu rata-rata panggilan fungsi meningkat karena ini; jadi titik plot sekarang naik dan ke kanan lagi.
Dari sini kita dapat menyimpulkan bahwa mengunci dan membuka kunci mutex setiap 50 ns bukanlah masalah pada kotak saya.
Kesimpulan saya adalah bahwa jawaban untuk pertanyaan OP adalah bahwa menambahkan lebih banyak mutex lebih baik selama itu menghasilkan lebih sedikit pertengkaran.
Cobalah untuk mengunci mutex sesingkat mungkin. Satu-satunya alasan untuk menempatkan mereka -say- di luar loop adalah jika loop itu loop lebih cepat dari sekali setiap 100 ns (atau lebih tepatnya, jumlah utas yang ingin menjalankan loop itu pada waktu yang sama kali 50 ns) atau ketika 13 ns kali ukuran lingkaran lebih banyak keterlambatan daripada penundaan yang Anda dapatkan dengan pertikaian.
EDIT: Saya mendapat lebih banyak pengetahuan tentang masalah ini sekarang dan mulai meragukan kesimpulan yang saya sampaikan di sini. Pertama-tama, CPU 0 dan 1 berubah menjadi hyper-threaded; meskipun AMD mengklaim memiliki 8 core nyata, pasti ada sesuatu yang sangat mencurigakan karena penundaan antara dua core lainnya jauh lebih besar (yaitu, 0 dan 1 membentuk pasangan, seperti halnya 2 dan 3, 4 dan 5, dan 6 dan 7 ). Kedua, std :: mutex diimplementasikan dengan cara memutar kunci sedikit sebelum benar-benar melakukan panggilan sistem ketika gagal untuk segera mendapatkan kunci pada mutex (yang tidak diragukan lagi akan sangat lambat). Jadi apa yang saya ukur di sini adalah situasi yang paling ideal dan dalam praktiknya, mengunci dan membuka kunci mungkin memerlukan waktu lebih drastis per kunci / membuka kunci.
Intinya, mutex diimplementasikan dengan atom. Untuk menyinkronkan atom antar core, bus internal harus dikunci yang membekukan jalur cache yang sesuai untuk beberapa ratus siklus clock. Dalam hal kunci tidak dapat diperoleh, panggilan sistem harus dilakukan untuk membuat utas tertidur; itu jelas sangat lambat (system calls dalam urutan 10 mircoseconds). Biasanya itu bukan masalah karena utas itu harus tidur - tapi itu bisa menjadi masalah dengan pertengkaran tinggi di mana utas tidak dapat memperoleh kunci untuk waktu yang biasanya berputar dan begitu pula sistem panggilan, tetapi BISA ambil kunci sesaat setelahnya. Misalnya, jika beberapa utas mengunci dan membuka kunci mutex dalam satu lingkaran ketat dan masing-masing menjaga kunci selama 1 mikrodetik atau lebih, maka mereka mungkin diperlambat secara luar biasa oleh fakta bahwa mereka terus-menerus ditidurkan dan dibangunkan lagi. Juga, sekali utas tidur dan utas lain harus membangunkannya, utas itu harus melakukan panggilan sistem dan ditunda ~ 10 mikrodetik; penundaan ini terjadi saat membuka kunci mutex ketika utas lain menunggu mutex itu di kernel (setelah berputar terlalu lama).
sumber
Ini tergantung pada apa yang Anda sebut "mutex", mode OS, dll.
Pada minimum itu adalah biaya operasi memori saling bertautan. Ini adalah operasi yang relatif berat (dibandingkan dengan perintah assembler primitif lainnya).
Namun, itu bisa sangat jauh lebih tinggi. Jika apa yang Anda sebut "mutex" objek kernel (yaitu - objek yang dikelola oleh OS) dan dijalankan dalam mode pengguna - setiap operasi di atasnya mengarah ke transaksi mode kernel, yang sangat berat.
Misalnya pada prosesor Intel Core Duo, Windows XP. Operasi yang saling terkait: membutuhkan sekitar 40 siklus CPU. Panggilan mode kernel (yaitu panggilan sistem) - sekitar 2000 siklus CPU.
Jika ini masalahnya - Anda dapat mempertimbangkan untuk menggunakan bagian kritis. Ini adalah hibrida dari mutex kernel dan akses memori yang saling terkait.
sumber
std::mutex
rata - rata menggunakan durasi (dalam detik) 10 kali lebih banyak dariint++
. Namun, saya tahu itu sulit dijawab karena sangat tergantung pada banyak hal.Biaya akan bervariasi tergantung pada implementasinya tetapi Anda harus mengingat dua hal:
Pada sistem prosesor tunggal, Anda biasanya dapat menonaktifkan interupsi cukup lama untuk mengubah data secara atom. Sistem multi-prosesor dapat menggunakan strategi uji-dan-set .
Dalam kedua kasus tersebut, instruksi relatif efisien.
Seperti apakah Anda harus menyediakan satu mutex tunggal untuk struktur data besar-besaran, atau memiliki banyak mutex, satu mutex untuk setiap bagiannya, itu adalah tindakan penyeimbang.
Dengan memiliki satu mutex, Anda memiliki risiko pertengkaran yang lebih tinggi antara banyak utas. Anda dapat mengurangi risiko ini dengan memiliki mutex per bagian tetapi Anda tidak ingin masuk ke situasi di mana utas harus mengunci 180 mutex untuk melakukan tugasnya :-)
sumber
Saya benar-benar baru dalam pthreads dan mutex, tetapi saya dapat mengonfirmasi dari eksperimen bahwa biaya mengunci / membuka kunci mutex hampir nihil ketika tidak ada pertentangan, tetapi ketika ada pertentangan, biaya pemblokiran sangat tinggi. Saya menjalankan kode sederhana dengan kumpulan utas tempat tugasnya hanya menghitung jumlah dalam variabel global yang dilindungi oleh kunci mutex:
Dengan satu utas, program ini menjumlahkan 10.000.000 nilai hampir secara instan (kurang dari satu detik); dengan dua utas (pada MacBook dengan 4 core), program yang sama membutuhkan waktu 39 detik.
sumber