Seberapa efisien penguncian mutex yang tidak dikunci? Berapa biaya mutex?

149

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

Albert
sumber
Ini akan menjadi implementasi dan spesifik arsitektur. Beberapa mutex hampir tidak memerlukan biaya apa pun jika ada dukungan perangkat keras asli, yang lain akan membutuhkan biaya banyak. Tidak mungkin menjawab tanpa informasi lebih lanjut.
Gian
2
@Gian: Ya, tentu saja saya menyiratkan pertanyaan ini dalam pertanyaan saya. Saya ingin tahu tentang perangkat keras umum tetapi juga pengecualian penting jika ada.
Albert
Saya benar-benar tidak melihat implikasi itu di mana pun. Anda bertanya tentang "instruksi assembler" - jawabannya bisa di mana saja dari 1 instruksi hingga sepuluh ribu instruksi tergantung pada arsitektur apa yang Anda bicarakan.
Gian
15
@Gian: Kalau begitu tolong berikan jawaban ini. Tolong katakan apa itu sebenarnya di x86 dan amd64, tolong beri contoh untuk arsitektur di mana itu adalah 1 instruksi dan berikan satu di mana itu 10k. Tidak jelas apakah saya ingin tahu itu dari pertanyaan saya?
Albert

Jawaban:

120

Saya memiliki pilihan di antara apakah memiliki banyak mutex atau satu untuk objek.

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.

Seberapa efisien untuk mengunci mutex? Yaitu berapa banyak instruksi assembler yang ada dan berapa banyak waktu yang mereka ambil (dalam hal mutex tidak terkunci)?

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.

Berapa biaya mutex? Apakah benar ada banyak mutex yang bermasalah? Atau bisakah saya membuang variabel mutex dalam kode saya karena saya memiliki variabel int dan itu tidak masalah?

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:

  1. Kurang kunci berarti lebih banyak pertengkaran (syscall lambat, warung CPU) dan paralelisme yang lebih rendah
  2. Kurang kunci berarti lebih sedikit masalah debugging masalah multi-threading.
  3. Semakin banyak kunci berarti semakin sedikit pertengkaran dan paralelisme yang lebih tinggi
  4. Semakin banyak kunci berarti semakin banyak peluang untuk mengalami kebuntuan yang tidak dapat dibatalkan.

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.

Dummy00001
sumber
Terima kasih, itu sebagian besar menjawab pertanyaan saya. Saya tidak tahu bahwa kernel (misalnya kernel Linux) menangani mutex dan Anda mengendalikannya melalui syscalls. Tetapi karena Linux sendiri mengelola penjadwalan dan konteks, ini masuk akal. Tetapi sekarang saya memiliki imajinasi kasar tentang apa yang akan dilakukan kunci mutex / unlock secara internal.
Albert
2
@Albert: Oh. Saya lupa konteks switch ... Konteks switch terlalu menguras kinerja. Jika akuisisi kunci gagal dan utas harus menunggu, itu terlalu separuh dari konteks. CS itu sendiri cepat, tetapi karena CPU dapat digunakan oleh beberapa proses lain, cache akan diisi dengan data alien. Setelah utas akhirnya mendapatkan kunci, kemungkinan bahwa untuk CPU harus memuat ulang hampir semua dari RAM lagi.
Dummy00001
@ Dummy00001 Beralih ke proses lain berarti Anda harus mengubah pemetaan memori CPU. Itu tidak murah.
curiousguy
27

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:

masukkan deskripsi gambar di sini

Ini menunjukkan hasil benchmark berjalan pada kode berikut:

uint64_t do_Ndec(int thread, int loop_count)
{
  uint64_t start;
  uint64_t end;
  int __d0;

  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
  mutex.lock();
  mutex.unlock();
  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
  asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
  return end - start;
}

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

Carlo Wood
sumber
10

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.

Valdo
sumber
7
Bagian kritis Windows jauh lebih dekat dengan mutex. Mereka memiliki semantik mutex biasa, tetapi mereka adalah proses-lokal. Bagian terakhir membuat mereka jauh lebih cepat, karena mereka dapat ditangani sepenuhnya dalam proses Anda (dan dengan demikian kode mode pengguna).
MSalters
2
Angka tersebut akan lebih berguna jika jumlah siklus CPU dari operasi umum (misalnya aritmatika / jika-lain / cache-miss / tipuan) juga disediakan untuk perbandingan. .... Akan lebih bagus lagi jika ada referensi nomornya. Di internet, sangat sulit untuk menemukan informasi tersebut.
javaLover
@javaLover Operasi tidak berjalan dalam siklus; mereka berjalan pada unit aritmatika untuk sejumlah siklus. Ini sangat berbeda. Biaya instruksi dalam waktu bukanlah kuantitas yang ditentukan, hanya biaya penggunaan sumber daya. Sumber daya ini dibagikan. Dampak dari instruksi memori tergantung banyak caching, dll.
curiousguy
@curiousguy Setuju. Saya tidak jelas. Saya ingin menjawab seperti std::mutexrata - rata menggunakan durasi (dalam detik) 10 kali lebih banyak dari int++. Namun, saya tahu itu sulit dijawab karena sangat tergantung pada banyak hal.
javaLover
6

Biaya akan bervariasi tergantung pada implementasinya tetapi Anda harus mengingat dua hal:

  • biayanya akan sangat minimal karena keduanya merupakan operasi yang cukup primitif dan akan dioptimalkan sebanyak mungkin karena pola penggunaannya ( banyak digunakan ).
  • tidak masalah seberapa mahal itu karena Anda harus menggunakannya jika Anda ingin operasi multi-threaded yang aman. Jika Anda membutuhkannya, maka Anda membutuhkannya.

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

paxdiablo
sumber
1
Ya, tapi seberapa efisien? Apakah ini instruksi mesin tunggal? Atau sekitar 10? Atau sekitar 100? 1000? Lebih? Semua ini masih efisien, namun dapat membuat perbedaan dalam situasi ekstrem.
Albert
1
Ya, itu tergantung sepenuhnya pada implementasinya. Anda dapat mematikan interupsi, menguji / mengatur bilangan bulat dan mengaktifkan kembali interupsi dalam satu lingkaran dalam sekitar enam instruksi mesin. Tes-dan-set dapat dilakukan dalam jumlah sebanyak karena prosesor cenderung menyediakan itu sebagai instruksi tunggal.
paxdiablo
Tes-dan-set bus-terkunci adalah instruksi tunggal (agak panjang) pada x86. Sisa dari mesin untuk menggunakannya cukup cepat ("apakah tes berhasil?" Adalah pertanyaan bahwa CPU bagus dalam melakukan cepat) tetapi panjang instruksi bus-terkunci yang benar-benar penting karena itu adalah bagian yang memblokir hal-hal. Solusi dengan interupsi jauh lebih lambat, karena memanipulasinya biasanya terbatas pada kernel OS untuk menghentikan serangan DoS yang sepele.
Donal Fellows
BTW, jangan gunakan drop / perolehan kembali sebagai sarana untuk memiliki hasil benang kepada orang lain; itu strategi yang payah di sistem multicore. (Ini salah satu hal yang relatif sedikit yang CPython salah.)
Donal Fellows
@ Donal: Apa yang Anda maksud dengan drop / reacquire? Kedengarannya penting; dapatkah Anda memberi saya lebih banyak informasi tentang itu?
Albert
5

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:

y = exp(-j*0.0001);
pthread_mutex_lock(&lock);
x += y ;
pthread_mutex_unlock(&lock);

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.

Grant Petty
sumber