Bu, bir derleyici bazı hedef makinelerde umduğunuz şeyi yapan kod üretse bile, C ++ 'ın Tanımlanmamış Davranışa neden olan bir Veri Yarışı olarak tanımladığı şeydir. std::atomic
Güvenilir sonuçlar için kullanmanız gerekir , ancak memory_order_relaxed
yeniden sıralamayı önemsemiyorsanız kullanabilirsiniz. Kullanarak bazı örnek kod ve asm çıktıları için aşağıya bakın fetch_add
.
Ama önce, montaj dili sorunun bir parçası:
Num ++ bir komut ( add dword [num], 1
) olduğundan, bu durumda num ++ 'ın atomik olduğu sonucuna varabilir miyiz?
Bellek hedef yönergeleri (salt depolar dışında), birden çok dahili adımda gerçekleşen okuma-değiştirme-yazma işlemleridir . Hiçbir mimari kayıt değiştirilmez, ancak CPU verileri ALU aracılığıyla gönderirken dahili olarak tutmalıdır . Gerçek kayıt dosyası, en basit CPU'nun içindeki veri depolama alanının sadece küçük bir parçasıdır ve mandallar, bir aşamanın çıkışlarını başka bir aşama için girişler olarak tutar, vb.
Diğer CPU'lardan gelen bellek işlemleri yük ve depolama arasında global olarak görülebilir. Yani add dword [num], 1
bir döngü içinde çalışan iki iş parçacığı birbirlerinin mağazalarına basar. ( Güzel bir diyagram için @ Margaret'in cevabına bakınız ). İki iş parçacığının her birinden 40 bin artıştan sonra sayaç, gerçek çok çekirdekli x86 donanımında yalnızca ~ 60 bin (80 bin değil) yükselmiş olabilir.
Yunancada bölünmez anlamına gelen “Atomik” hiçbir gözlemcinin operasyonu ayrı adımlar olarak göremeyeceği anlamına gelir . Tüm bitler için aynı anda fiziksel / elektriksel olarak gerçekleşmek, bunu bir yük veya depolama için başarmanın sadece bir yoludur, ancak bu bir ALU işlemi için bile mümkün değildir. X86'daki Atomicity'ye cevabımda saf yükler ve saf depolar hakkında daha fazla ayrıntıya girerken, bu cevap okuma-değiştir-yaz üzerine odaklanıyor.
Ön lock
ek , tüm işlemin sistemdeki tüm olası gözlemcilere göre (diğer çekirdekler ve DMA aygıtları, CPU pinlerine bağlanmış bir osiloskop değil) atomik hale getirilmesi için birçok okuma-değiştirme-yazma (bellek hedefi) komutuna uygulanabilir. Bu yüzden var. (Ayrıca bkz. Bu soru-cevap ).
Yani lock add dword [num], 1
bir atom . Bu komutu çalıştıran bir CPU çekirdeği, yükün önbellekten veri okuduğu andan depo sonucunu tekrar önbelleğe verene kadar önbellek satırını Özel L1 önbelleğinde Değiştirilmiş durumda sabitlenmiş tutacaktır. Bu, MESI önbellek tutarlılık protokolünün (veya çok çekirdekli AMD / tarafından kullanılan MOESI / MESIF sürümlerinin) kurallarına göre, sistemdeki diğer herhangi bir önbelleğin, yükten depoya herhangi bir noktada önbellek hattının bir kopyasına sahip olmasını önler . Intel CPU'lar). Böylece, diğer çekirdeklerin operasyonları sırasında değil, öncesinde ya da sonrasında gerçekleşir.
Ön lock
ek olmadan , başka bir çekirdek önbellek hattının sahipliğini alabilir ve yükümüzden sonra ancak mağazamızdan önce değiştirebilir, böylece diğer mağaza yük ve depomuz arasında küresel olarak görünür hale gelir. Diğer bazı cevaplar bunu yanlış anlar ve lock
sizden aynı önbellek satırının çakışan kopyalarını alacağınızı iddia eder . Bu hiçbir zaman tutarlı önbelleklere sahip bir sistemde gerçekleşemez.
( lock
Ed talimatı iki önbellek satırını kapsayan bellekte çalışıyorsa, nesnenin her iki parçasındaki değişikliklerin tüm gözlemcilere yayıldıkça atomik kaldığından emin olmak çok daha fazla iş gerektirir, böylece hiçbir gözlemci yırtılmayı göremez. Veriler belleğe ulaşana kadar tüm bellek veri yolunu kilitlemeniz gerekir. Atomik değişkenlerinizi yanlış hizalamayın!)
Not, lock
ön ek, aynı zamanda (örneğin bir tam bellek bariyere bir yönerge döner MFENCE tüm çalışma zamanı yeniden sıralama ve böylece sıralı olan kıvamı sağlamak üzere durdurma). (Bkz. Jeff Preshing'in mükemmel blog yazısı . Diğer yazıları da mükemmel ve x86 ve diğer donanım detaylarından C ++ kurallarına kadar kilitsiz programlama hakkında birçok iyi şeyi açık bir şekilde açıklıyor .)
Tek işlemcili makinede ya da tek iş parçacıklı süreçte , tek RMW talimat aslında olan bir olmadan atomik lock
önek. Diğer kodun paylaşılan değişkene erişmesinin tek yolu, CPU'nun bir komutun ortasında gerçekleşemeyecek bir bağlam anahtarı yapmasıdır. Böylece bir düz dec dword [num]
, tek iş parçacıklı bir program ile sinyal işleyicileri arasında veya tek çekirdekli bir makinede çalışan çok iş parçacıklı bir programda senkronize olabilir. Bkz başka soru üzerine cevabım ikinci yarısını ve ben daha ayrıntılı olarak açıklamak altındaki yorum,.
Geri C ++:
num++
Derleyiciye tek bir okuma-değiştirme-yazma uygulamasına derlemeniz gerektiğini söylemeden kullanmak tamamen sahte :
;; Valid compiler output for num++
mov eax, [num]
inc eax
mov [num], eax
Bu, num
daha sonra değerini kullanırsanız çok olasıdır : derleyici, artıştan sonra bir kayıtta canlı tutacaktır. Bu nedenle num++
, kendi başına derleme yöntemini kontrol etseniz bile , çevresindeki kodu değiştirmek kodu etkileyebilir.
Değeri sonradan gerekli değilse ( inc dword [num]
tercih edilir; Modern x86 CPU'lar üç ayrı talimatları kullanarak verimli şekilde, en az bellek hedef RMW talimat çalışacak Eğlenceli gerçek:. gcc -O3 -m32 -mtune=i586
Aslında bu yayacaktır , (Pentium) P5 en superscalar boru hattı DEĞİL Mİ çünkü P6 ve sonraki mikro mimarilerin yaptığı gibi çok sayıda basit mikro-operasyon için karmaşık talimatları deşifre etmeyin.Daha fazla bilgi için Agner Sis'in talimat tablolarına / mikro-mimari kılavuzuna bakın.x86 wiki'yi birçok yararlı bağlantı için etiketleyin (PDF olarak ücretsiz olarak sunulan Intel'in x86 ISA kılavuzları dahil).
Hedef bellek modelini (x86) C ++ bellek modeliyle karıştırmayın
Derleme zamanı yeniden sıralamasına izin verilir . Std :: atomic ile elde ettiğiniz diğer bölüm, derleme zamanı yeniden sıralaması üzerinde kontroldürnum++
.
Klasik örnek: Başka bir iş parçacığının bakması için bazı verileri arabelleğe kaydetme, ardından bir bayrak ayarlama. X86 ücretsiz olarak yük / serbest bırakma depoları edinmesine rağmen, derleyiciye kullanarak yeniden sıralamamasını söylemelisiniz flag.store(1, std::memory_order_release);
.
Bu kodun diğer evrelerle senkronize edilmesini bekliyor olabilirsiniz:
// flag is just a plain int global, not std::atomic<int>.
flag--; // This isn't a real lock, but pretend it's somehow meaningful.
modify_a_data_structure(&foo); // doesn't look at flag, and the compilers knows this. (Assume it can see the function def). Otherwise the usual don't-break-single-threaded-code rules come into play!
flag++;
Ama olmayacak. Derleyici flag++
işlev çağrısında hareket etmekte serbesttir (eğer işlevi satırlarsa veya bakmadığını bilirse flag
). O zaman değişikliği tamamen optimize edebilir, çünkü flag
değil volatile
. (Ve hayır, C ++ volatile
, std :: atomic için yararlı bir alternatif değildir. Std :: atomic, derleyicinin bellekteki değerlerin eşzamansız olarak değiştirilebileceğini varsaymasını sağlar volatile
, ancak bundan daha fazlası volatile std::atomic<int> foo
yoktur. aynı şekilde std::atomic<int> foo
@Richard Hodges ile ele alındığı gibi,.)
Atom olmayan değişkenler üzerindeki veri yarışlarını Tanımsız Davranış olarak tanımlamak, derleyicinin hala yükleri kaldırmasını ve depoları döngülerden kaldırmasını ve birden çok iş parçacığının referans alabileceği bellek için diğer birçok optimizasyonu sağlar. ( UB'nin derleyici optimizasyonunu nasıl etkinleştirdiği hakkında daha fazla bilgi için bu LLVM bloguna bakın .)
Bahsettiğim gibi, x86 lock
öneki bir tam bellek bariyeri olduğundan, num.fetch_add(1, std::memory_order_relaxed);
x86'da num++
(varsayılan sıralı tutarlılıktır) aynı kodu üretir , ancak diğer mimarilerde (ARM gibi) çok daha verimli olabilir. X86'da bile, rahat, daha derleme zamanı yeniden sıralama sağlar.
std::atomic
Küresel bir değişken üzerinde çalışan birkaç işlev için GCC'nin x86 üzerinde yaptığı şey budur .
Godbolt derleyici gezgininde güzel biçimlendirilmiş kaynak + derleme dil koduna bakın . Bu hedefler için atomlardan ne tür bir montaj dili kodu aldığınızı görmek için ARM, MIPS ve PowerPC dahil olmak üzere diğer hedef mimarileri seçebilirsiniz.
#include <atomic>
std::atomic<int> num;
void inc_relaxed() {
num.fetch_add(1, std::memory_order_relaxed);
}
int load_num() { return num; } // Even seq_cst loads are free on x86
void store_num(int val){ num = val; }
void store_num_release(int val){
num.store(val, std::memory_order_release);
}
// Can the compiler collapse multiple atomic operations into one? No, it can't.
# g++ 6.2 -O3, targeting x86-64 System V calling convention. (First argument in edi/rdi)
inc_relaxed():
lock add DWORD PTR num[rip], 1 #### Even relaxed RMWs need a lock. There's no way to request just a single-instruction RMW with no lock, for synchronizing between a program and signal handler for example. :/ There is atomic_signal_fence for ordering, but nothing for RMW.
ret
inc_seq_cst():
lock add DWORD PTR num[rip], 1
ret
load_num():
mov eax, DWORD PTR num[rip]
ret
store_num(int):
mov DWORD PTR num[rip], edi
mfence ##### seq_cst stores need an mfence
ret
store_num_release(int):
mov DWORD PTR num[rip], edi
ret ##### Release and weaker doesn't.
store_num_relaxed(int):
mov DWORD PTR num[rip], edi
ret
Sıralı tutarlılık depolarından sonra MFENCE'ın (tam bir bariyer) nasıl gerekli olduğuna dikkat edin. x86 genel olarak şiddetle sipariş edilir, ancak StoreLoad'ın yeniden düzenlenmesine izin verilir. Bir ardışık olmayan CPU'da iyi performans elde etmek için bir mağaza arabelleğine sahip olmak çok önemlidir. Jeff Preshing en Kanunda yakalandı Hafıza yeniden sıralama sonuçlarını gösterir değil gerçek donanım üzerinde oluyor yeniden sıralama göstermek için gerçek kodla MFENCE kullanarak.
Re: @Richard Hodges'in std :: atomic num++; num-=2;
operasyonlarını tek bir num--;
talimatta birleştiren derleyiciler hakkındaki cevabı hakkındaki yorumlarda tartışma :
Aynı konuda ayrı bir Soru-Cevap: Derleyiciler neden gereksiz std :: atomic yazmaları birleştirmiyor? , cevabım aşağıda yazdıklarımın çoğunu ifade ediyor.
Mevcut derleyiciler bunu yapmıyorlar (henüz), ancak izin verilmediği için değil. C ++ WG21 / P0062R1: Derleyiciler ne zaman atomikleri optimize etmelidir? Birçok programcının derleyicilerin "şaşırtıcı" optimizasyon yapmayacağı beklentisini ve standardın programcılara kontrol vermek için neler yapabileceğini tartışır. N4455 , bu da dahil olmak üzere optimize edilebilecek birçok şeyden bahsediyor . Satır içi ve sürekli yayılmanın , orijinal kaynakta açıkça fazla atom opsleri olmasa bile, fetch_or(0)
sadece bir load()
(ancak yine de anlambilim elde edebilir) dönüştürebilecek şeyler ortaya çıkarabileceğine işaret eder .
Derleyicilerin bunu yapmamasının gerçek nedenleri (henüz): (1) derleyicinin bunu güvenli bir şekilde yapmasına izin verecek karmaşık kodu (hiç yanlış anlamadan) yazmamış ve (2) potansiyel olarak en azından prensibini ihlal etmemiştir. sürpriz . Kilitsiz kod ilk etapta doğru yazmak için yeterince zordur. Atomik silah kullanımında rahat olmayın: ucuz değiller ve fazla optimize etmiyorlar. Bununla birlikte std::shared_ptr<T>
, atomik olmayan bir sürümü olmadığından, gereksiz atomik işlemlerden kaçınmak her zaman kolay değildir (buradaki cevaplardan birishared_ptr_unsynchronized<T>
gcc için a tanımlamak için kolay bir yol verse de).
İçin arkasını alınıyor num++; num-=2;
o sanki derleme num--
Derleyiciler: izin verilir sürece, bunu yapmak num
olduğunu volatile std::atomic<int>
. Yeniden sıralama mümkün ise, as-if kuralı derleyicinin derleme zamanında her zaman bu şekilde olduğuna karar vermesine izin verir . Hiçbir şey bir gözlemcinin ara değerleri ( num++
sonuç) görebileceğini garanti etmez .
Yani, bu işlemler arasında küresel olarak hiçbir şeyin görünür olmadığı sıralama, kaynağın sipariş gereklilikleriyle uyumluysa (soyut mimarinin C ++ kurallarına göre, hedef mimari yerine), derleyici / lock dec dword [num]
yerine tek bir tane yayabilir .lock inc dword [num]
lock sub dword [num], 2
num++; num--
hala gözüken diğer iş parçacıkları ile bir Eşitleme ilişkisi var num
ve bu iş parçacığındaki diğer işlemlerin yeniden sıralanmasını engelleyen hem bir edinme yükü hem de bir yayın deposu var. X86 için, bu bir lock add dword [num], 0
(ie num += 0
) yerine bir MFENCE için derleme yapabilir .
PR0062'de tartışıldığı gibi , bitişik olmayan atomik op'ların derleme zamanında daha agresif bir şekilde birleştirilmesi kötü olabilir (örneğin, bir ilerleme sayacı her yineleme yerine yalnızca sonunda bir kez güncellenir), ancak aynı zamanda dezavantajsız performansa yardımcı olabilir (örn. a'nın bir kopyası shared_ptr
oluşturulduğunda ve yok edildiğinde, derleyicinin shared_ptr
geçici sürenin tüm ömrü boyunca başka bir nesnenin var olduğunu kanıtlayabilmesi durumunda, atomun inc / dec değeri sayılır .)
num++; num--
Bir iş parçacığı bile , bir iş parçacığı hemen kilidini açıp yeniden kilitlediğinde kilit uygulamasının adaletine zarar verebilir. Asm'de gerçekten serbest bırakılmazsa, donanım tahkim mekanizmaları bile başka bir iş parçacığına o noktada kilidi alma şansı vermez.
Mevcut gcc6.2 ve clang3.9 lock
ile memory_order_relaxed
, en belirgin şekilde optimize edilebilir durumda bile yine de ayrı ed işlemleri elde edersiniz . ( Godbolt derleyici gezgini, böylece en son sürümlerin farklı olup olmadığını görebilirsiniz.)
void multiple_ops_relaxed(std::atomic<unsigned int>& num) {
num.fetch_add( 1, std::memory_order_relaxed);
num.fetch_add(-1, std::memory_order_relaxed);
num.fetch_add( 6, std::memory_order_relaxed);
num.fetch_add(-5, std::memory_order_relaxed);
//num.fetch_add(-1, std::memory_order_relaxed);
}
multiple_ops_relaxed(std::atomic<unsigned int>&):
lock add DWORD PTR [rdi], 1
lock sub DWORD PTR [rdi], 1
lock add DWORD PTR [rdi], 6
lock sub DWORD PTR [rdi], 5
ret
add
atomik olduğunu kim söyledi ?