Kilidi açılmış bir muteksi kilitlemek ne kadar verimli? Muteksin maliyeti nedir?


149

Düşük seviyeli bir dilde (C, C ++ veya her neyse): Bir grup muteksi (pthread'in bana verdiği şey veya yerel sistem kütüphanesinin sağladığı gibi) ya da bir nesne için tek bir tane arasında seçim yapabilirsiniz.

Muteksi kilitlemek ne kadar verimli? Yani kaç montaj talimatı olabilir ve ne kadar zaman alırlar (muteksin kilidinin açılması durumunda)?

Muteksin maliyeti nedir? Gerçekten çok fazla muteksiye sahip olmak bir problem mi? Ya da sadece değişkenlerim olduğu gibi koduma mutex değişkenleri atabilir miyim intve gerçekten önemli değil mi?

(Farklı donanımlar arasında ne kadar fark olduğundan emin değilim. Varsa, onlar hakkında da bilmek isterim. Ama çoğunlukla, ortak donanımlarla ilgileniyorum.)

Mesele şu ki, her nesne için tek bir muteks yerine nesnenin sadece bir kısmını kaplayan birçok muteks kullanarak, birçok bloğu güvence altına alabilirdim. Ve bu konuda ne kadar ilerlemem gerektiğini merak ediyorum. Yani, ne kadar daha karmaşık ve ne kadar daha fazla muteks olursa olsun, mümkün olan her bloğu gerçekten mümkün olduğunca korumaya çalışmalıyım?


Kilitleme ile ilgili WebKits blog yazısı (2016) bu soru ile çok ilgilidir ve bir spinlock, adaptif kilit, futex vb. Arasındaki farkları açıklar.


Bu uygulama ve mimariye özgü olacak. Bazı muteksler yerel donanım desteği varsa neredeyse hiçbir maliyeti olmayacak, diğerleri çok pahalıya mal olacak. Daha fazla bilgi olmadan cevap vermek imkansızdır.
Gian

2
@Gian: Tabii ki sorumdaki bu alt soruyu ima ediyorum. Ben ortak donanım hakkında bilmek istiyorum ama aynı zamanda dikkate değer istisnalar varsa.
Albert

Gerçekten bu sonucu hiçbir yerde göremiyorum. "Montajcı talimatları" hakkında soruyorsunuz - cevap, hangi mimariden bahsettiğinize bağlı olarak 1 komuttan on bin talimata kadar her yerde olabilir.
Gian

15
@Gian: O zaman lütfen tam olarak bu cevabı ver. Lütfen aslında x86 ve amd64'te ne olduğunu söyleyin, lütfen 1 talimat olduğu bir mimariye örnek verin ve 10k olduğu yerde bir mimari verin. Bunu sorumdan bilmek istediğim belli değil mi?
Albert

Yanıtlar:


120

Ben bir grup mutexes veya bir nesne için tek bir tane arasında seçim var.

Çok sayıda iş parçacığınız varsa ve nesneye erişim sık sık gerçekleşirse, birden çok kilit paralelliği artıracaktır. Daha fazla kilitleme, kilitlemenin daha fazla hata ayıklaması anlamına geldiğinden, bakım maliyeti pahasına.

Muteksi kilitlemek ne kadar verimli? Yani ne kadar montaj talimatı vardır ve ne kadar zaman alırlar (muteksin kilidinin açılması durumunda)?

Kesin montajcı talimatları muteksin en az ek yüküdür - bellek / önbellek tutarlılık garantileri ana ek yüktür. Ve daha az sıklıkla belirli bir kilit alınır - daha iyi.

Muteks iki ana bölümden oluşur (aşırı basitleştirme): (1) muteksin kilitli olup olmadığını gösteren bir bayrak ve (2) kuyruk bekle.

Bayrağın değiştirilmesi sadece birkaç talimattır ve normalde sistem çağrısı olmadan yapılır. Muteks kilitliyse, çağrı dizisini bekleme kuyruğuna eklemek ve beklemeye başlamak için sistem çağrısı yapılır. Bekleme kuyruğu boşsa kilidini açmak ucuzdur, ancak aksi halde bekleme işlemlerinden birini uyandırmak için bir sistem çağrısına ihtiyaç duyar. (Bazı sistemlerde, muteksleri uygulamak için ucuz / hızlı sistem çağrıları kullanılır, yalnızca çekişme durumunda yavaş (normal) sistem çağrıları haline gelir.)

Kilidi açılmış muteksi kilitlemek gerçekten ucuz. Çekişme olmadan muteks kilidini açmak da ucuzdur.

Muteksin maliyeti nedir? Gerçekten çok fazla muteksiye sahip olmak bir problem mi? Veya int değişkenleri olduğu gibi kodumda mutex değişkenleri kadar atmak ve gerçekten önemli değil mi?

Kodunuza istediğiniz kadar muteks değişkeni atabilirsiniz. Yalnızca uygulamanızın ayırabileceği bellek miktarı ile sınırlısınız.

Özet. Kullanıcı alanı kilitleri (ve özellikle muteksler) ucuzdur ve herhangi bir sistem sınırına tabi değildir. Fakat çok fazla kişi hata ayıklamak için kabus görüyor. Basit tablo:

  1. Daha az kilit, daha fazla içerik (yavaş sistem çağrıları, CPU durakları) ve daha az paralellik anlamına gelir
  2. Daha az kilit, çoklu iş parçacığı sorunlarını gidermek için daha az sorun anlamına gelir.
  3. Daha fazla kilit daha az çekişme ve daha yüksek paralellik demektir
  4. Daha fazla kilit, tartışılmaz kilitlenmelere girme şansını arttırır.

Genellikle # 2 ve # 3 dengelenerek, uygulama için dengeli bir kilitleme şeması bulunmalı ve sürdürülmelidir.


(*) Daha az kilitli muteksler ile ilgili sorun, uygulamanızda çok fazla kilitleme varsa, CPU / çekirdek trafiğinin çoğunun muteks belleği diğer CPU'ların veri önbelleğinden temizlemesini sağlamaktır. önbellek tutarlılığı. Önbellek sifonları hafif kesintiler gibidir ve CPU'lar tarafından şeffaf bir şekilde ele alınır - ancak sözde tezgahları tanıtırlar ("durak" araması).

Ve tezgahlar, kilit kodunun yavaş çalışmasını sağlayan şeydir, genellikle uygulamanın neden yavaş olduğuna dair herhangi bir belirti olmadan. (Bazı arklar CPU arası / çekirdek trafik istatistiklerini sağlar, bazıları sağlamaz.)

Problemden kaçınmak için, insanlar genellikle kilit çekişme olasılığını azaltmak ve duraktan kaçınmak için çok sayıda kilide başvururlar. Bu nedenle, sistem sınırlarına tabi olmayan ucuz kullanıcı alanı kilitlemesinin var olmasının nedeni budur.


Teşekkürler, bu çoğunlukla soruma cevap veriyor. Çekirdeğin (örneğin Linux çekirdeği) muteksleri işlediğini ve bunları sistem çağrılarıyla kontrol ettiğinizi bilmiyordum. Ancak Linux'un kendisi zamanlama ve bağlam anahtarlarını yönetirken, bu mantıklıdır. Ama şimdi muteks kilitlemenin / kilidinin dahili olarak ne yapacağına dair kaba bir hayal gücüm var.
Albert

2
@Albert: Oh. Bağlam anahtarlarını unuttum ... Bağlam anahtarları performansta çok drenaj yapıyor. Kilit alımı başarısız olursa ve iş parçacığının beklemesi gerekiyorsa, bu bağlam anahtarının yarısıdır. CS'nin kendisi hızlıdır, ancak CPU başka bir işlem tarafından kullanılabileceğinden, önbellekler yabancı verilerle doldurulur. İş parçacığı sonunda kilidi elde ettikten sonra, CPU'nun RAM'den hemen hemen her şeyi yeniden yüklemesi gerekecek.
Dummy00001

@ Dummy00001 Başka bir işleme geçmek, CPU'nun bellek eşlemelerini değiştirmeniz gerektiği anlamına gelir. O kadar ucuz değil.
curiousguy

27

Aynı şeyi bilmek istedim, bu yüzden ölçtüm. Kutumda (AMD FX (tm) -8150 3.612361 GHz'de Sekiz Çekirdekli İşlemci), kendi önbellek satırında bulunan ve zaten önbelleğe alınmış olan kilidi açılmış bir muteksi kilitlemek ve kilidini açmak 47 saat (13 ns) alır.

İki çekirdek arasındaki senkronizasyon nedeniyle (CPU # 0 ve # 1'i kullandım), iki iş parçacığında 102 ns'de bir kez bir kilitleme / kilit açma çiftini çağırabilirim, bu yüzden her 51 ns'de bir, kabaca 38 aldığı sonucuna varabilir ns bir iş parçacığı bir sonraki iş parçacığı yeniden kilitlemek önce bir kilit açma yaptıktan sonra kurtarmak için.

Bunu araştırmak için kullandığım program burada bulunabilir: https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx

Kutum için belirli birkaç sabit kodlu değere sahip olduğunu unutmayın (xrange, yrange ve rdtsc ek yükü), bu yüzden muhtemelen sizin için çalışmadan önce bunu denemeniz gerekir.

Bu durumda ürettiği grafik:

resim açıklamasını buraya girin

Bu, karşılaştırma kodunun aşağıdaki kodda çalışmasının sonucunu gösterir:

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;
}

İki rdtsc çağrısı, mutex'i kilitlemek ve kilidini açmak için gereken saat sayısını ölçer (kutumdaki rdtsc aramaları için 39 saat yükü ile). Üçüncü asm bir gecikme döngüsüdür. Gecikme döngüsünün boyutu, iplik 1 için iplik 0 için olduğundan 1 sayı daha küçüktür, bu nedenle iplik 1 biraz daha hızlıdır.

Yukarıdaki işlev, 100.000 büyüklüğünde sıkı bir döngüde çağrılır. İşlevin iş parçacığı 1 için biraz daha hızlı olmasına rağmen, muteks çağrısı nedeniyle her iki döngü de eşitlenir. Bu, grafikte, kilitleme / kilit açma çifti için ölçülen saat sayısının, altındaki döngüdeki daha kısa gecikmeyi hesaba katmak için iplik 1 için biraz daha fazla olması gerçeğinden görülebilir.

Yukarıdaki grafikte sağ alt nokta 150 gecikme loop_count değerine sahip bir ölçümdür ve daha sonra alttaki noktaları sola doğru takip ederek loop_count her ölçümde bir azaltılır. 77 olduğunda fonksiyon her iki iş parçacığında da 102 ns olarak adlandırılır. Daha sonra loop_count daha da azaltılırsa, dişleri senkronize etmek artık mümkün değildir ve muteks çoğu zaman gerçekten kilitlenmeye başlar, bu da kilitleme / kilidini açmak için gereken saatlerin artmasına neden olur. Ayrıca fonksiyon çağrısının ortalama süresi bu nedenle artar; böylece çizim noktaları tekrar yukarı ve sağa doğru gidiyor.

Buradan, her 50 ns'de bir muteksi kilitlemenin ve kilidini açmanın kutumda bir sorun olmadığı sonucuna varabiliriz.

Sonuç olarak, OP sorusunun cevabı, daha az çekişme ile sonuçlandığı sürece daha fazla muteks eklemenin daha iyi olduğudur.

Muteksleri olabildiğince kısa tutmaya çalışın. Onları bir döngü dışına koymanın tek nedeni, bu döngü 100 ns'den bir kez (veya daha doğrusu, aynı döngüyü 50 ns kez çalıştırmak isteyen iş parçacığı sayısından) veya 13 ns'den daha hızlı döngüye girerse döngü boyutu, çekişme yoluyla aldığınız gecikmeden daha fazla gecikmedir.

DÜZENLEME: Şimdi konuyla ilgili çok daha bilgili oldum ve burada sunduğum sonuçtan şüphe etmeye başladım. Her şeyden önce, CPU 0 ve 1 hiper iş parçacıklıdır; AMD'nin 8 gerçek çekirdeğe sahip olduğunu iddia etmesine rağmen, kesinlikle çok balıklı bir şey var çünkü diğer iki çekirdek arasındaki gecikmeler çok daha büyük (yani 0 ve 1, 2 ve 3, 4 ve 5 ve 6 ve 7 gibi bir çift oluşturuyor) ). İkincisi, std :: mutex, bir muteks üzerindeki kilidi hemen alamadığında sistem çağrıları yapmadan önce kilitleri biraz döndürecek şekilde uygulanır (şüphesiz son derece yavaş olacaktır). Yani burada ölçtüğüm kesinlikle en ideal durum ve pratikte kilitleme ve kilit açma kilit / kilit açma başına büyük ölçüde daha fazla zaman alabilir.

Sonuç olarak, atomlarla bir muteks uygulanır. Çekirdekler arasındaki atomları senkronize etmek için, birkaç önbellek hattını birkaç yüz saat döngü boyunca donatan bir dahili veri yolu kilitlenmelidir. Bir kilidin elde edilememesi durumunda, ipliği uyku moduna geçirmek için bir sistem çağrısı yapılmalıdır; bu son derece yavaştır (sistem çağrıları 10 mirisaniyedir). Normalde bu gerçekten bir sorun değildir, çünkü iş parçacığının yine de uyuması gerekir - ancak bir iş parçacığının normalde döndüğü süre boyunca kilidi alamadığı ve sistem çağrısı yaptığı yüksek çekişme ile ilgili bir sorun olabilir, ancak CAN kısa süre sonra kilidi al. Örneğin, birkaç iş parçacığı bir muteksi sıkı bir döngüde kilitleyip kilitlerse ve her biri kilidi 1 mikrosaniye kadar tutarsa, o zaman sürekli uyumaları ve tekrar uyandırılmaları nedeniyle çok yavaşlayabilirler. Ayrıca, bir iş parçacığı uyuduğunda ve başka bir iş parçacığının onu uyandırması gerektiğinde, bu iş parçacığı bir sistem çağrısı yapmak zorundadır ve ~ 10 mikrosaniye geciktirilir; dolayısıyla bu gecikme, çekirdekteki başka bir iş parçacığını beklerken bir muteksin kilidini açarken olur (eğirme çok uzun sürdükten sonra).


10

Bu aslında "muteks", işletim sistemi modu vb.

En azından , birbirine bağlı bir bellek işleminin maliyeti. Nispeten ağır bir işlemdir (diğer ilkel birleştirici komutlarına kıyasla).

Ancak, bu çok daha yüksek olabilir. Eğer "mutex" Bir çekirdek nesne dediğimiz (yani - OS tarafından yönetilen nesne) kullanıcı modunda ve çalıştırmak - Üzerinde her operasyon, bir çekirdek modu işlem yol açar ki bu çok ağır .

Örneğin Intel Core Duo işlemcide, Windows XP. Kilitli çalışma: yaklaşık 40 CPU döngüsü sürer. Çekirdek modu çağrısı (yani sistem çağrısı) - yaklaşık 2000 CPU çevrimi.

Bu durumda - kritik bölümleri kullanmayı düşünebilirsiniz. Bir çekirdek muteksi ve kilitli bellek erişimi melezidir.


7
Windows kritik bölümleri mutekslere çok daha yakındır. Düzenli muteks semantiği vardır, ancak süreç yereldir. Son bölüm, onları tamamen işleminizde (ve dolayısıyla kullanıcı modu kodunda) ele alınabildiklerinden onları çok daha hızlı hale getirir.
MSalters

2
Karşılaştırma için ortak işlemlerin CPU döngülerinin miktarı (örneğin, aritmetik / if-else / önbellek-özledim / dolaylı) sağlandığında sayı daha yararlı olacaktır. .... Sayının bazı referansları olsa bile harika olurdu. İnternette bu tür bilgileri bulmak çok zordur.
javaLover

@javaLover İşlemler döngüde çalışmaz; bir dizi döngü için aritmetik birimlerde çalışırlar. Çok farklı. Herhangi bir talimatın zaman içindeki maliyeti tanımlanmış bir miktar değildir, sadece kaynak kullanım maliyeti. Bu kaynaklar paylaşılıyor. Bellek talimatlarının etkisi çok fazla önbelleğe alma vb.
Bağlıdır

@curiousguy Kabul ediyorum. Net değildim. std::mutexOrtalama kullanım süresi (saniyede) 10 kat daha fazla gibi cevap istiyorum int++. Ancak, cevap vermenin zor olduğunu biliyorum çünkü bu büyük ölçüde bir şeye bağlı.
javaLover

6

Maliyet uygulamaya bağlı olarak değişecektir, ancak iki şeyi aklınızda bulundurmalısınız:

  • hem oldukça ilkel bir operasyon olduğundan hem de kullanım şekli nedeniyle mümkün olduğunca optimize edileceğinden maliyet büyük olasılıkla minimum olacaktır ( çok kullanılır) ) .
  • güvenli çok iş parçacıklı bir işlem istiyorsanız kullanmak zorunda olduğunuz için ne kadar pahalı olduğu önemli değildir. İhtiyacınız varsa, ihtiyacınız var.

Tek işlemcili sistemlerde, genellikle verileri atomik olarak değiştirecek kadar uzun kesintileri devre dışı bırakabilirsiniz. Çok işlemcili sistemler bir test ve set stratejisi kullanabilir.

Her iki durumda da, talimatlar nispeten etkilidir.

Büyük bir veri yapısı için tek bir muteks sağlamanız gerekip gerekmediği ya da her bir bölümü için bir tane olmak üzere birçok muteksinizin olması, dengeleyici bir eylemdir.

Tek bir muteks ile, birden çok iş parçacığı arasında daha yüksek çekişme riski vardır. Bölüm başına bir muteks ile bu riski azaltabilirsiniz, ancak bir iş parçacığının işini yapmak için 180 muteks'i kilitlemesi gereken bir duruma girmek istemezsiniz :-)


1
Evet, ama ne kadar verimli? Tek bir makine talimatı mı? Ya da yaklaşık 10 mu? Ya da yaklaşık 100? 1000? Daha? Tüm bunlar hala etkilidir, ancak aşırı durumlarda bir fark yaratabilir.
Albert

1
Bu tamamen uygulamaya bağlıdır . Kesintileri kapatabilir, bir tamsayıyı test edebilir / ayarlayabilir ve yaklaşık altı makine talimatında bir döngüdeki kesintileri yeniden etkinleştirebilirsiniz. Test ve set yaklaşık olarak yapılabilir, çünkü işlemciler bunu tek bir talimat olarak sağlama eğilimindedir.
paxdiablo

Veri yolu kilitli test ve set, x86'da tek (oldukça uzun) bir talimattır. Kullanılacak makinelerin geri kalanı oldukça hızlıdır (“test başarılı oldu mu?” CPU'ların hızlı yapmakta iyi olduğu bir sorudur), ancak şeyleri engelleyen kısım olduğu için gerçekten önemli olan otobüs kilitli talimatın uzunluğu. Kesintileri olan çözümler çok daha yavaştır, çünkü bunları manipüle etmek genellikle önemsiz DoS saldırılarını durdurmak için işletim sistemi çekirdeğiyle sınırlıdır.
Donal Fellows

BTW, başkalarına iplik verimi için bir araç olarak düşme / tekrarlama kullanmayın; bu çok çekirdekli bir sistem için berbat bir stratejidir. (CPython'un yanlış yaptığı nispeten az şeyden biri.)
Donal Fellows

@Donal: Bırakma / yeniden kazanma ile ne demek istiyorsun? Kulağa önemli geliyor; bana bu konuda daha fazla bilgi verebilir misin?
Albert

5

Ben pthreads ve mutex için tamamen yeniyim, ancak deneylerden bir muteksi kilitlemenin / kilidini açma maliyetinin çekişme olmadığında neredeyse zilch olduğunu doğrulayabilirim, ancak çekişme olduğunda, engelleme maliyeti son derece yüksektir. Ben görev sadece mutex kilidi tarafından korunan bir küresel değişken bir toplam hesaplamak olduğu bir iş parçacığı havuzu ile basit bir kod koştu:

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

Bir iş parçacığıyla, program neredeyse anlık olarak 10.000.000 değeri toplar (bir saniyeden az); iki iş parçacığı olan (4 çekirdekli bir MacBook'ta), aynı program 39 saniye sürer.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.