Num ++ 'int num' için atomik olabilir mi?


153

Genel olarak int num, num++(veya ++num) için, bir okuma-değiştirme-yazma işlemi olarak, atomik değildir . Ancak sık sık derleyiciler, örneğin GCC , bunun için aşağıdaki kodu oluşturmak görüyorum ( burada deneyin ):

Resim açıklamasını buraya girin

5. sıraya tekabül eden num++bir talimat olduğundan, bu durumda num++ bunun atomik olduğu sonucuna varabilir miyiz ?

Ve eğer öyleyse, bu şekilde oluşturulan num++, herhangi bir veri yarışları tehlikesi olmadan eşzamanlı (çok iş parçacıklı) senaryolarda kullanılabileceği anlamına mı gelir (örneğin, bunu yapmamıza std::atomic<int>ve ilişkili maliyetleri empoze etmemize gerek yoktur. neyse atomik)?

GÜNCELLEME

Uyarı Bu soru olduğunu değil artım olmadığını ise (bu değil ve olmasıydı ve söz açılış çizgisidir) atomik. O olmadığını bulunuyor olabilir bazı durumlarda tek talimat doğa kutu yükü önlemek için istismar edilebilir olup olmadığı, yani belirli senaryolarda olmak lockönek. Kabul cevap tek işlemcili makineler hakkında bölüm yanı sıra bahseder gibi Ve bu cevap , onun görüş ve diğerlerinde konuşma açıklamak elinden (++ değil C veya C rağmen).


65
Bunun addatomik olduğunu kim söyledi ?
Slava

6
Atomik özelliklerden birinin optimizasyon sırasında belirli yeniden sıralamanın önlenmesi olduğu göz önüne alındığında, hayır, gerçek operasyonun
atomisitesinden

19
Ben de böyle olduğunu işaret etmek istiyorum eğer bu platformda atom olan başka pltaform üzerinde olacağı garantisi yoktur. Platformdan bağımsız olun ve niyetinizi a std::atomic<int>.
NathanOliver

8
Bu addkomutun yürütülmesi sırasında , başka bir çekirdek, bu çekirdeğin önbelleğinden bellek adresini çalabilir ve değiştirebilir. Bir x86 CPU'da, işlemin süresi boyunca adresin önbellekte kilitlenmesi gerekiyorsa addyönerge bir lockönek gerektirir.
David Schwartz

21
Herhangi bir işlemin "atomik" olması mümkündür . Yapmanız gereken tek şey şanslı olmak ve atomik olmadığını ortaya çıkaracak hiçbir şeyi asla yürütmemek. Atomik sadece bir garanti olarak değerlidir . Eğer montaj koda bakarken göz önüne alındığında, soru o belirli mimarisi size garanti sağlamak için olur olup olmadığıdır ve derleyici olduğunu seçtikleri montaj seviyesi uygulaması olduğunu garanti sağlayıp sağlamadığını.
Cort Ammon

Yanıtlar:


197

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::atomicGüvenilir sonuçlar için kullanmanız gerekir , ancak memory_order_relaxedyeniden 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], 1bir 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 lockek , 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 lockek 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 locksizden 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.

( lockEd 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, numdaha 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=i586Aslı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. 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ü flagdeğ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> fooyoktur. 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::atomicKü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 numolduğ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 numve 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_ptroluşturulduğunda ve yok edildiğinde, derleyicinin shared_ptrgeç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 lockile 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

1
"[ayrı talimatları kullanarak] daha verimli olurdu ... ama modern x86 CPU'lar RMW işlemlerini bir kez daha en azından verimli bir şekilde idare ediyor" - güncellenen değerin daha sonra aynı işlevde kullanılması durumunda yine de daha verimli ve derleyicinin saklaması için ücretsiz bir kayıt mevcuttur (ve değişken elbette geçici olarak işaretlenmez). Bu , derleyicinin işlem için tek bir komut veya birden fazla komut üretip üretmediğinin, yalnızca söz konusu tek satıra değil, işlevdeki kodun geri kalanına bağlı olması olasılığının yüksek olduğu anlamına gelir .
Periata Breatta

@PeriataBreatta: evet, iyi bir nokta. Asm'de mov eax, 1 xadd [num], eaxartım uygulamak için (kilit öneki olmadan) kullanabilirsiniz num++, ancak derleyiciler bunu yapmaz.
Peter Cordes

3
@ DavidC.Rankin: Yapmak istediğiniz düzenlemeleriniz varsa, çekinmeyin. Yine de bu CW yapmak istemiyorum. Hala benim işim (ve dağınıklığım: P). Ultimate [frizbi] oyunumdan sonra biraz toparlayacağım :)
Peter Cordes

1
Topluluk wiki değilse, uygun etiket wiki'sinde bir bağlantı olabilir. (hem x86 hem de atomik etiketler?). SO üzerinde genel bir arama ile umut verici bir dönüş yerine ek bağlantıya değer (Eğer bu konuda uygun olması gerektiğini daha iyi biliyorsam, bunu yaparım. wiki linkage)
David C. Rankin

1
Her zamanki gibi - harika cevap! Tutarlılık ve atomisite arasında iyi bir ayrım (bazılarının yanlış yaptığı yerlerde)
Leeor

39

... ve şimdi optimizasyonları etkinleştirelim:

f():
        rep ret

Tamam, bir şans verelim:

void f(int& num)
{
  num = 0;
  num++;
  --num;
  num += 6;
  num -=5;
  --num;
}

sonuç:

f(int&):
        mov     DWORD PTR [rdi], 0
        ret

başka bir gözlem parçasının (önbellek senkronizasyon gecikmelerini göz ardı ederek bile) bireysel değişiklikleri gözlemleme fırsatı yoktur.

karşılaştırmak:

#include <atomic>

void f(std::atomic<int>& num)
{
  num = 0;
  num++;
  --num;
  num += 6;
  num -=5;
  --num;
}

sonuç:

f(std::atomic<int>&):
        mov     DWORD PTR [rdi], 0
        mfence
        lock add        DWORD PTR [rdi], 1
        lock sub        DWORD PTR [rdi], 1
        lock add        DWORD PTR [rdi], 6
        lock sub        DWORD PTR [rdi], 5
        lock sub        DWORD PTR [rdi], 1
        ret

Şimdi, her değişiklik: -

  1. başka bir iş parçacığında gözlemlenebilir ve
  2. diğer iş parçacıklarında benzer değişikliklere saygılı.

atomisite sadece talimat düzeyinde değildir, işlemciden önbelleklere, belleğe ve arkaya kadar tüm boru hattını içerir.

Daha fazla bilgi

std::atomicS güncellemelerinin optimizasyonunun etkisi ile ilgili olarak .

C ++ standardı vardır o sipariş koduna derleyici caizdir, hatta kod sonucu olması şartıyla yeniden hangi kural 'sanki' tam aynı gözlemlenebilir basitçe infaz sanki (yan etkiler dahil) etkileri senin kodu.

As-if kuralı muhafazakardır, özellikle de atomiklerle ilgilidir.

düşünmek:

void incdec(int& num) {
    ++num;
    --num;
}

Muteks kilitleri, atomlar veya zincirler arası sıralamayı etkileyen başka yapılar olmadığından, derleyicinin bu işlevi bir NOP olarak yeniden yazmakta özgür olduğunu iddia ederim, örneğin:

void incdec(int&) {
    // nada
}

Bunun nedeni, c ++ bellek modelinde, artışın sonucunu gözlemleyen başka bir iş parçacığının olasılığı yoktur. Eğer tabii farklı olurdu numoldu volatile(kudreti etkisi donanım davranışı). Ancak bu durumda, bu işlev bu belleği değiştiren tek işlev olacaktır (aksi takdirde program kötü biçimlendirilmiştir).

Ancak, bu farklı bir top oyunu:

void incdec(std::atomic<int>& num) {
    ++num;
    --num;
}

numbir atom. Değişikliklerin izlediği diğer evrelerde gözlemlenmesi gerekir . Bu iş parçacıklarının kendisinde yaptığı değişikliklerin (artış ve azalma arasında değeri 100 olarak ayarlamak gibi), num.

İşte bir demo:

#include <thread>
#include <atomic>

int main()
{
    for (int iter = 0 ; iter < 20 ; ++iter)
    {
        std::atomic<int> num = { 0 };
        std::thread t1([&] {
            for (int i = 0 ; i < 10000000 ; ++i)
            {
                ++num;
                --num;
            }
        });
        std::thread t2([&] {
            for (int i = 0 ; i < 10000000 ; ++i)
            {
                num = 100;
            }
        });
        
        t2.join();
        t1.join();
        std::cout << num << std::endl;
    }
}

örnek çıktı:

99
99
99
99
99
100
99
99
100
100
100
100
99
99
100
99
99
100
100
99

5
Bu açıklamaya başarısız add dword [rdi], 1olduğu değil (olmadan atom lockönek). Yük atomiktir ve depo atomiktir, ancak hiçbir şey başka bir iş parçacığının yük ve depo arasındaki verileri değiştirmesini durduramaz. Böylece mağaza başka bir iş parçacığı tarafından yapılan bir modifikasyona basabilir. Bkz. Jfdube.wordpress.com/2011/11/30/anlayici-otomatik- operasyonlar . Ayrıca, Jeff Preshing'in kilitsiz makaleleri son derece iyi ve bu giriş makalesinde temel RMW probleminden bahsediyor.
Peter Cordes

3
Burada gerçekten olan hiç kimse bu optimizasyonu gcc'de uygulamıyor, çünkü neredeyse işe yaramaz ve muhtemelen yardımcı olmaktan daha tehlikeli olurdu. (Prensip az sürpriz. Belki birisi olan geçici devlet bazen görünür olmasını bekliyor ve istatistiksel olasılık ile Tamam. Ya da vardır modifikasyon üzerine kesmek için donanım izle-noktalarını kullanarak.) Kilidi serbest kod ihtiyaçlarını dikkatle hazırlanmış olması, optimize etmek için hiçbir şey olmayacak. Kodlayıcıların kodlarının düşündükleri anlamına gelmeyebileceği konusunda uyarıcıyı uyarmak ve bir uyarı yazdırmak yararlı olabilir!
Peter Cordes

2
Bu, derleyicilerin bunu uygulamamasının bir nedeni olabilir (en azından sürpriz ilkesi vb.). Bunun gerçek donanımda pratikte mümkün olacağını gözlemleyerek. Bununla birlikte, C ++ bellek sipariş kuralları, bir iş parçacığının yüklerinin C ++ soyut makinedeki diğer iş parçacığı op'leriyle "eşit olarak" karıştığının garantisi hakkında hiçbir şey söylemez. Hala yasal olacağını düşünüyorum, ama programcı düşmanı.
Peter Cordes

2
Düşünce deneyi: İşbirlikçi çoklu görev sistemi üzerinde bir C ++ uygulaması düşünün. Kilitlenmeleri önlemek için gereken yerlere verim noktaları ekleyerek std :: thread'i uygular, ancak her komut arasında değil. Sana C o şey iddia ediyorum tahmin ++ standardı arasında bir verim noktası gerektirir num++ve num--. Standartta bunu gerektiren bir bölüm bulabilirseniz, bunu halledecektir. Eminim ki hiçbir gözlemci yanlış bir yeniden sıralama göremez, bu da orada verim gerektirmez. Bence bu sadece bir uygulama kalitesi meselesi.
Peter Cordes

5
Son olarak, std tartışma posta listesine sordum. Bu soru, Peter ile hemfikir gibi görünen 2 makaleyi ortaya çıkardı ve bu tür optimizasyonlarla ilgili endişelerimi dile getirdi: wg21.link/p0062 ve wg21.link/n4455 Bunları dikkatime çeken Andy'ye teşekkürler.
Richard Hodges

38

Birçok komplikasyon olmadan add DWORD PTR [rbp-4], 1, CISC tarzı bir talimattır .

Üç işlem gerçekleştirir: işleneni bellekten yükleyin, artırın, işleneni tekrar belleğe kaydedin.
Bu işlemler sırasında CPU veri yolunu iki kez alır ve serbest bırakır, diğer herhangi bir ajan arasında da onu alabilir ve bu atomikliği ihlal eder.

AGENT 1          AGENT 2

load X              
inc C
                 load X
                 inc C
                 store X
store X

X sadece bir kez arttırılır.


7
@LeoHeinsaar Durumun böyle olabilmesi için, her bellek yongasının kendi Aritmetik Mantık Birimi'ne (ALU) ihtiyacı olacaktır. Bu, aslında, her bellek yongası olduğunu gerektirecektir oldu bir işlemci.
Richard Hodges

6
@ LeoHeinsaar: bellek-hedef talimatları 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 depolamasının sadece küçük bir parçasıdır ve mandallar, bir aşamadaki çıkışları başka bir aşama için girişler olarak tutar, vb.
Peter Cordes

@PeterCordes Yorumunuz tam olarak aradığım cevap. Margaret'in cevabı, böyle bir şeyin içeride devam etmesi gerektiğinden şüphelenmemi sağladı.
Leo Heinsaar

Bu yorumu, sorunun C ++ kısmını ele almak da dahil olmak üzere tam bir cevaba dönüştürdü.
Peter Cordes

1
@PeterCordes Teşekkürler, çok ayrıntılı ve her konuda. Açıkçası bir veri yarışı ve bu nedenle C ++ standardı tarafından tanımlanmamış bir davranıştı, sadece oluşturulan kodun yayınladığı şeyin atomik vb. Olabileceğini varsayabileceğini merak ettim. kılavuzlar atomisiteyi , bellek işlemleriyle ilgili olarak çok açık bir şekilde tanımlamaktadır ve varsayımımla değil, “kilitli işlemler diğer tüm bellek işlemlerine ve tüm harici olarak görülebilir olaylara göre atomiktir.”
Leo Heinsaar

11

Ekleme talimatı atomik değildir . Belleğe başvurur ve iki işlemci çekirdeği bu belleğin farklı yerel önbelleğine sahip olabilir.

IIRC ekleme talimatının atomik varyantına kilit xadd denir


3
lock xaddfetch_addeski değeri döndürerek C ++ std :: atomic uygular . Buna ihtiyacınız yoksa, derleyici normal bellek hedefi talimatlarını bir lockönekle kullanır. lock addveya lock inc.
Peter Cordes

1
add [mem], 1önbelleği olmayan bir SMP makinesinde hala atomik olmaz, diğer cevaplar hakkındaki yorumlarıma bakın.
Peter Cordes

Tam olarak nasıl atomik olmadığı hakkında daha fazla ayrıntı için cevabımı görün. Ayrıca benim cevabın sonu bu ilgili soru üzerine .
Peter Cordes

10

Num ++ 'a karşılık gelen 5. satır bir komut olduğundan, num ++' ın bu durumda atomik olduğu sonucuna varabilir miyiz?

"Tersine mühendislik" tarafından üretilen montajı temel alan sonuçlar çıkarmak tehlikelidir. Örneğin, kodunuzu optimizasyon devre dışı bırakılmış olarak derlemişsinizdir, aksi takdirde derleyici bu değişkeni atmış veya 1'i çağırmadan doğrudan ona yüklemiştir operator++. Oluşturulan montaj, optimizasyon bayraklarına, hedef CPU'ya vb. Göre önemli ölçüde değişebileceğinden, sonucunuz kuma dayanır.

Ayrıca, bir montaj talimatının bir işlemin atomik olduğu anlamına geldiği fikri de yanlıştır. Bu add, x86 mimarisinde bile çoklu CPU sistemlerinde atomik olmayacak.


9

Derleyiciniz bunu her zaman bir atomik işlem olarak yayınlasa bile, numaynı anda başka bir iş parçacığından erişim C ++ 11 ve C ++ 14 standartlarına göre bir veri yarışı oluşturur ve programın tanımlanmamış davranışı olur.

Ama bundan daha kötü. Birincisi, daha önce de belirtildiği gibi, bir değişkeni arttırırken derleyici tarafından oluşturulan talimat, optimizasyon seviyesine bağlı olabilir. İkinci olarak, derleyici atomik değilse , diğer bellek erişimlerini yeniden sıralayabilir , ör.++numnum

int main()
{
  std::unique_ptr<std::vector<int>> vec;
  int ready = 0;
  std::thread t{[&]
    {
       while (!ready);
       // use "vec" here
    });
  vec.reset(new std::vector<int>());
  ++ready;
  t.join();
}

İyimser bir şekilde ++readybunun "atomik" olduğunu ve derleyicinin kontrol döngüsünü gerektiği gibi oluşturduğunu varsaysak bile (dediğim gibi, bu UB'dir ve bu nedenle derleyici onu kaldırmak, sonsuz bir döngü ile değiştirmekte serbesttir), derleyici yine de işaretçi atamasını taşıyabilir veya vectorartırım işleminden sonra bir noktaya başlatılmasını daha da kötüleştirerek yeni iş parçacığında kaosa neden olabilir. Uygulamada, optimize edici bir derleyici readydeğişkeni ve kontrol döngüsünü tamamen kaldırdıysa hiç şaşırmam , çünkü bu dil kuralları (gözlemlenecek özel durumların aksine) altında gözlemlenebilir davranışları etkilemez.

Aslında, geçen yılın Meeting C ++ konferansında, iki derleyici geliştiricisinden, küçük bir performans iyileştirmesi görüldüğünde bile, dil kurallarının izin verdiği sürece, naif olarak yazılmış çok iş parçacıklı programları yanlış davranan optimizasyonları çok memnuniyetle uyguladıklarını duydum. doğru yazılmış programlarda.

Son olarak, hatta eğer sen taşınabilirlik umursamadı, ve derleyici sihirli güzeldi, kullandığınız işlemci çok muhtemel bir superscalar CISC tiptedir ve mikro-op, sipariülerde ve / veya spekülatif onları yürütmek içine talimatları aşağı kıracak, yalnızca LOCKsaniyedeki işlemleri en üst düzeye çıkarmak için (Intel'de) önek veya bellek çitleri gibi ilkellerin senkronize edilmesiyle sınırlıdır .

Uzun bir hikaye kısaca anlatmak gerekirse, iş parçacığı güvenli programlamanın doğal sorumlulukları şunlardır:

  1. Göreviniz, dil kuralları (ve özellikle dil standart bellek modeli) altında iyi tanımlanmış bir davranışa sahip kod yazmaktır.
  2. Derleyicinizin görevi, hedef mimarinin bellek modeli altında aynı iyi tanımlanmış (gözlemlenebilir) davranışa sahip makine kodu oluşturmaktır.
  3. CPU'nuzun görevi, gözlenen davranışın kendi mimarisinin bellek modeliyle uyumlu olması için bu kodu çalıştırmaktır.

Kendi yolunuzla yapmak istiyorsanız, sadece bazı durumlarda işe yarayabilir, ancak garantinin geçersiz olduğunu anlayın ve istenmeyen sonuçlardan yalnızca siz sorumlu olacaksınız . :-)

Not: Doğru yazılmış örnek:

int main()
{
  std::unique_ptr<std::vector<int>> vec;
  std::atomic<int> ready{0}; // NOTE the use of the std::atomic template
  std::thread t{[&]
    {
       while (!ready);
       // use "vec" here
    });
  vec.reset(new std::vector<int>());
  ++ready;
  t.join();
}

Bu güvenlidir çünkü:

  1. Kontrolleri readydil kurallarına göre optimize edilemez.
  2. ++ready Önce-olmuyor görür çek readydeğil sıfır olarak, ve diğer işlemler bu işlemleri etrafında yeniden sıralanmış edilemez. Bunun nedeni ++readyve kontrolün ardışık olarak tutarlı olması , bu da C ++ bellek modelinde açıklanan başka bir terimdir ve bu özel yeniden sıralamayı yasaklar. Dolayısıyla derleyici talimatları sipariş ve ayrıca örneğin yazı göndermekte erteleme gerektiğini CPU söylemek gerekir olmamalıdır vecartımı sonrası için ready. Sıralı olarak tutarlı olan, dil standardındaki atomlarla ilgili en güçlü garantidir. Daha düşük (ve teorik olarak daha ucuz) garantiler, örn.std::atomic<T>, ancak bunlar yalnızca uzmanlar içindir ve derleyici geliştiricileri tarafından çok fazla optimize edilmeyebilir, çünkü nadiren kullanılırlar.

1
Derleyici tüm kullanımlarını göremezse ready, muhtemelen while (!ready);daha çok benzer bir şekilde derlenir if(!ready) { while(true); }. Upvoted: std :: atomic'in önemli bir kısmı, herhangi bir noktada eşzamansız değişiklik yapmak için anlambilimi değiştiriyor. Normalde UB olması, derleyicilerin yükleri kaldırmasını ve depoları döngülerden batırmasını sağlayan şeydir.
Peter Cordes

9

Tek çekirdekli bir x86 makinesinde, bir addtalimat genellikle CPU 1'deki diğer koda göre atomik olacaktır . Bir kesinti, tek bir komutu ortaya çıkaramaz.

Tek bir çekirdek içinde sırayla tek bir yürütme talimatlarının yanılsamasını korumak için sıra dışı yürütme gereklidir, bu nedenle aynı CPU'da çalışan herhangi bir talimat ekleme işleminden önce veya tamamen sonra gerçekleşir.

Modern x86 sistemleri çok çekirdekli olduğundan, tek işlemcili özel durum geçerli değildir.

Biri küçük bir gömülü PC'yi hedefliyorsa ve kodu başka bir şeye taşımayı planlamıyorsa, "ekle" komutunun atomik yapısı kullanılabilir. Öte yandan, operasyonların doğası gereği atomik olduğu platformlar gittikçe azalmaktadır.

C ++ da konum yazma if (Gerçi, sana yardım etmez. Derleyiciler gerektirecek bir seçeneği yok num++bir bellek hedef eklenti için derlemek veya xadd için olmadan bir lockönek. Onlar yüklemek için tercih edebilirsiniz numbir kayıt ve mağaza içine artış sonucunu ayrı bir talimatla gösterir ve sonucu kullanırsanız büyük olasılıkla yapar.)


Dipnot 1: lockI / O aygıtları CPU ile aynı anda çalıştığı için orijinal 8086'da bile önek vardı; tek çekirdekli bir sistemdeki sürücülerin lock add, aygıt bunu değiştirebiliyorsa veya DMA erişimiyle ilgili olarak, aygıt belleğindeki bir değeri atomik olarak artırması gerekir .


Genelde atomik bile değildir: Başka bir evre aynı değişkeni aynı anda güncelleyebilir ve sadece bir güncelleme devralınır.
fuz

1
Çok çekirdekli bir sistem düşünün. Tabii ki, bir çekirdek içinde, talimat atomiktir, ancak tüm sisteme göre atomik değildir.
fuz

1
@FUZxxl: Cevabımın dördüncü ve beşinci kelimeleri nelerdi?
supercat

1
@supercat Cevabınız çok yanıltıcıdır, çünkü günümüzde yalnızca tek bir çekirdeğin nadir görülen durumunu dikkate alır ve OP'ye sahte bir güvenlik hissi verir. Bu yüzden çok çekirdekli davayı da dikkate aldım.
fuz

1
@FUZxxl: Bunun normal modern çok çekirdekli işlemcilerden bahsetmediğini fark etmeyen okuyucular için olası karışıklığı gidermek için bir düzenleme yaptım. (Ve ayrıca supercat'in emin olmadığı bazı şeyler hakkında daha spesifik olun). BTW, bu cevaptaki her şey zaten benimkinde, okuma-yazma-yazma işlemlerinin nasıl "atomik" olduğu platformlarının nasıl nadir olduğu hakkında son cümle hariç.
Peter Cordes

7

X86 bilgisayarların bir CPU'ya sahip olduğu günlerde, tek bir komutun kullanılması kesmelerin okuma / değiştirme / yazma işlemlerini bölmemesini ve bellek de DMA tamponu olarak kullanılmayacaksa, aslında atomikti (ve C ++ standartta iş parçacıklarından bahsetmedi, bu yüzden bu ele alınmadı).

Bir müşteri masaüstünde çift işlemciye (örneğin çift soketli Pentium Pro) sahip olmak nadir olduğunda, bunu tek çekirdekli bir makinede KİLİT önekinden kaçınmak ve performansı artırmak için etkili bir şekilde kullandım.

Bugün, yalnızca aynı CPU benzeşimine ayarlanmış birden çok iş parçacığına karşı yardımcı olacaktır, bu nedenle endişelendiğiniz iş parçacıkları yalnızca zaman diliminin süresi dolduğunda ve aynı iş parçacığında (çekirdek) diğer iş parçacığını çalıştırarak devreye girer. Bu gerçekçi değil.

Modern x86 / x64 işlemcilerle, tek talimat birkaç mikro opere bölünür ve ayrıca bellek okuma ve yazma arabelleğe alınır. Farklı CPU üzerinde çalışan Yani farklı ipler sadece sigara atomik olarak görmek olmaz ama bellekten okur neyi ve diğer ipler zamanla bu noktaya okudum kabul neyi ilgili tutarsız sonuçlar görebilirsiniz: Eklemek gerekir bellek çitler SANE geri davranışı.


1
Onlar çok Kesmeler hala değil bölünmüş RMW işlemlerini yapmak yok hala sinyal işleyici ile tek iplik senkronize aynı iş parçacığı içinde çalıştırmak. Tabii ki, bu sadece asm ayrı yük / değiştirme / depolama değil, tek bir talimat kullanıyorsa çalışır. C ++ 11 bu donanım işlevselliğini açığa çıkarabilir, ancak muhtemelen (Uniprocessor çekirdeğinde kesme işleyicileri ile senkronize etmenin gerçekten yararlı olduğu için, sinyal işleyicileriyle kullanıcı alanında değil). Ayrıca mimarilerin okuma-değiştirme-yazma bellek-hedef talimatları yoktur. Yine de, x86 olmayanlarda rahat bir atomik RMW gibi derlenebilir
Peter Cordes

Hatırladığım gibi, Kilit önekini kullanmak, üst düzey oyuncular gelene kadar saçma bir şekilde pahalı değildi. Bu nedenle, bu program tarafından gerekli olmasa da, 486'daki önemli kodu yavaşlatmak için bir neden yoktu.
JDługosz

Evet üzgünüm! Aslında dikkatlice okumadım. Paragrafın başlangıcını, uops'a kod çözme hakkında kırmızı ringa ile gördüm ve gerçekte ne söylediğini görmek için okumayı bitirmedim. re: 486: Sanırım en eski SMP'nin bir çeşit Compaq 386 olduğunu okudum, ancak bellek düzenindeki anlambilimi, x86 ISA'nın şu anda söylediği gibi değildi. Mevcut x86 kılavuzları SMP 486'dan bile bahsedebilir. Sanırım PPro / Athlon XP günlerine kadar HPC'de (Beowulf kümeleri) bile yaygın değildi.
Peter Cordes

1
@PeterCordes Tamam. Elbette, hiçbir DMA / cihaz gözlemcisi olmadığını varsayarak, bunu da dahil etmek için yorum alanına sığmadı. Mükemmel ekleme için teşekkürler JDługosz (cevap yanı sıra yorumlar). Tartışmayı gerçekten tamamladım.
Leo Heinsaar

3
@Leo: Bahsedilmemiş önemli bir nokta: sıra dışı CPU'lar işleri dahili olarak yeniden sıralar, ancak altın kural, tek bir çekirdek için , sırayla birer birer çalıştırılan talimatların yanılsamasını koruduklarıdır. (Ve bu, bağlam anahtarlarını tetikleyen kesintileri içerir). Değerler elektriksel olarak hafızanın içinde saklanabilir, ancak her şeyin üzerinde çalıştığı tek çekirdek, yanılsamayı korumak için yaptığı tüm yeniden siparişleri takip eder. Bu nedenle a = 1; b = a;, az önce depoladığınız 1'i doğru yüklemek için asm eşdeğeri için bir bellek bariyerine ihtiyacınız yoktur .
Peter Cordes

4

Hayır. Https://www.youtube.com/watch?v=31g0YE61PLQ (Bu, "Ofis" ten "Hayır" sahnesine bir bağlantı)

Bunun program için olası bir çıktı olacağını kabul ediyor musunuz?

örnek çıktı:

100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100

Eğer öyleyse, derleyici hangi dersten olursa olsun, program için mümkün olan tek çıktıyı yapmakta serbesttir . yani sadece 100'leri ortaya koyan bir main ().

Bu "olduğu gibi" kuralıdır.

Çıktıya bakılmaksızın, iplik senkronizasyonunu aynı şekilde düşünebilirsiniz - A num++; num--;iş parçacığı ve B iş parçacığı numtekrar tekrar okuyorsa, olası geçerli bir serpiştirme B iş parçacığının num++ve arasında asla okumamasıdır num--. Bu serpiştirme geçerli olduğundan, derleyici bunu mümkün olan tek serpiştirme yapmakta serbesttir . Ve sadece incr / decr'i tamamen kaldırın.

Burada bazı ilginç çıkarımlar var:

while (working())
    progress++;  // atomic, global

(yani dayalı bazı diğer iplik Güncellemelerimizi ilerleme çubuğu UI hayal progress)

Derleyici bunu şuna çevirebilir mi:

int local = 0;
while (working())
    local++;

progress += local;

muhtemelen bu geçerlidir. Ama muhtemelen programcının umduğu gibi değil :-(

Komite hala bu konular üzerinde çalışıyor. Şu anda "çalışıyor" çünkü derleyiciler atomları fazla optimize etmiyor. Ama bu değişiyor.

Ve progressayrıca değişken olsa bile , bu hala geçerli olacaktır:

int local = 0;
while (working())
    local++;

while (local--)
    progress++;

: - /


Bu cevap sadece Richard'la birlikte düşündüğümüz yan soruyu cevaplıyor gibi görünüyor. Sonunda çözdük: evet, C ++ standardı ,volatile başka kuralları ihlal etmediğinde, atom olmayan nesneler üzerindeki işlemlerin birleştirilmesine izin veriyor . İki standart tartışma belgesi tam olarak bunu tartışıyor ( Richard'ın yorumundaki bağlantılar ), biri aynı ilerleme sayacı örneğini kullanıyor. Bu nedenle, C ++ önleme yollarını standart hale getirene kadar uygulama kalitesi sorunu.
Peter Cordes

Evet, "Hayır" ım gerçekten tüm akıl yürütme yolunun bir cevabıdır. Eğer soru sadece "bazı derleyici / uygulama üzerinde num ++ atomik olabilir" ise, cevap emindir. Örneğin, bir derleyici lockher işleme eklemeye karar verebilir . Ya da her ikisinin de yeniden sıralanmadığı (yani "iyi günler") bazı derleyici + tek işlemcili kombinasyon atomik değildir. Ama bunun anlamı ne? Gerçekten ona güvenemezsin. Eğer sizin yazdığınız sistem olduğunu bilmiyorsanız. (O zaman bile, atomik <int> bu sisteme fazladan ops eklememesi daha iyi olur. Bu yüzden hala standart kod yazmalısınız ...)
tony

1
Bunun And just remove the incr/decr entirely.doğru olmadığını unutmayın . Hala bir edinme ve bırakma operasyonu num. X86 üzerinde num++;num--kesinlikle hiçbir şey değil, sadece MFENCE için derlemek, ama olabilir. (Derleyicinin tüm program analizi, num'nin bu modifikasyonu ile hiçbir şeyin senkronize olmadığını ve bundan önceki bazı mağazaların bundan sonra yüklerden sonraya kadar geciktirilmesinin önemi olmadığını kanıtlayamazsa.) Örneğin, bu bir kilit açma ve yeniden oluşturma -lock-right-away use-case, hala iki ayrı kritik bölümünüz var (belki mo_relaxed kullanarak), büyük bir bölüm değil.
Peter Cordes

@PeterCordes ah evet, kabul etti.
tony

2

Evet ama...

Atomik demek istediğin şey değil. Muhtemelen yanlış şeyi soruyorsunuzdur.

Artış kesinlikle atomiktir . Depolama birimi yanlış hizalanmadıkça (ve derleyiciye hizalamayı bıraktığınız için değil), mutlaka tek bir önbellek satırında hizalanmış olur. Önbelleğe alınmayan özel akış yönergelerinden kısa, her yazma önbellekten geçer. Tam önbellek satırları atomik olarak okunur ve yazılır, asla farklı bir şey değildir.
Önbellekten küçük veriler elbette atomik olarak da yazılır (çevreleyen önbellek satırı olduğu için).

İplik güvenli mi?

Bu farklı bir soru ve kesin bir "Hayır!" .

İlk olarak, başka bir çekirdeğin L1'de bu önbellek satırının bir kopyasına sahip olma olasılığı vardır (L2 ve yukarı genellikle paylaşılır, ancak L1 normalde çekirdek başınadır!) Ve eşzamanlı olarak bu değeri değiştirir. Tabii ki atomik olarak da oluyor, ama şimdi iki "doğru" (doğru, atomik, değiştirilmiş) değeriniz var - hangisi şimdi gerçekten doğru olanı?
CPU elbette bir şekilde çözecektir. Ancak sonuç beklediğiniz gibi olmayabilir.

İkincisi, bellek sıralaması vardır veya garantilerden önce farklı bir şekilde gerçekleşir. Atomik talimatlarla ilgili en önemli şey, atomik oldukları kadar fazla değildir . Sipariş veriyor.

Bellek açısından gerçekleşen her şeyin "daha önce gerçekleşmiş" bir garantiye sahip olduğunuz bazı garantili, iyi tanımlanmış bir sırayla gerçekleştiğine dair bir garantiyi yürütebilirsiniz. Bu sıralama "rahat" (şu şekilde okuyun: hiç yok) veya ihtiyacınız kadar katı olabilir.

Örneğin, bir veri bloğuna (örneğin, bazı hesaplamaların sonuçları) bir işaretçi ayarlayabilir ve ardından "veri hazır" bayrağını atomik olarak serbest bırakabilirsiniz . Şimdi, bu bayrağı kim edinirse , işaretçinin geçerli olduğunu düşünmeye yönlendirilir. Ve aslında, her zaman geçerli bir işaretçi olacak, asla farklı bir şey olmayacak. Çünkü işaretçiye yazma işlemi gerçekleşti - atomik işlemden önce.


2
Yük ve deponun her biri ayrı ayrı atomiktir, ancak bir bütün olarak okuma-değiştirme-yazma işleminin tamamı kesinlikle atomik değildir . Önbellekler tutarlıdır, bu nedenle asla aynı satırın çakışan kopyalarını tutamazsınız ( en.wikipedia.org/wiki/MESI_protocol ). Başka bir çekirdeğin salt okunur bir kopyası bile olamaz, bu çekirdeğin Değiştirilmiş durumda olması gerekir. Atomik olmayan şey, RMW'yi yapan çekirdeğin, yük ve mağaza arasındaki önbellek hattının sahipliğini kaybedebilmesidir.
Peter Cordes

2
Ayrıca, hayır, tüm önbellek hatları her zaman atomik olarak aktarılmaz. Bkz bu cevabı bile bile, deneysel çoklu priz Opteron HyperTransport 8B parçalar önbellek hatlarını aktararak 16B SSE depolar olmayan atomik hale getirdiğini gösterdi oluyor, olan (aynı tipte tek soketli işlemciler için atomik yük nedeniyle / mağaza donanımının L1 önbelleğine giden 16B yolu vardır). x86 sadece ayrı yükler veya 8B'ye kadar depolar için atomisiteyi garanti eder.
Peter Cordes

Derleyiciye hizalama bırakmak, belleğin 4 baytlık sınırda hizalanacağı anlamına gelmez. Derleyiciler, hizalama sınırını değiştirmek için seçeneklere veya pragmalara sahip olabilir. Bu, örneğin ağ akışlarında sıkıca paketlenmiş veriler üzerinde çalışmak için kullanışlıdır.
Dmitry Rubanovich

2
Sofistler, başka bir şey yok. Örnekte gösterildiği gibi bir yapının parçası olmayan otomatik depolamalı bir tamsayı kesinlikle doğru şekilde hizalanacaktır. Farklı bir şey iddia etmek tamamen aptalca. Önbellek çizgileri ve tüm POD'lar, dünyadaki hayali olmayan herhangi bir mimaride PoT (iki güç) boyutunda ve hizalanmıştır. Math, uygun şekilde hizalanmış herhangi bir PoT'nin aynı boyutta veya daha büyük herhangi bir PoT'dan tam olarak (asla daha fazla) uymamasına sahiptir. Bu yüzden ifadem doğrudur.
Damon

1
@Damon, soruda verilen örnek bir yapıdan bahsetmiyor, ancak soruyu sadece tamsayıların yapıların parçası olmadığı durumlarla daraltmıyor. POD'lar kesinlikle PoT boyutuna sahip olabilir ve PoT hizalı olmayabilir. Sözdizimi örnekleri için bu cevaba göz atın: stackoverflow.com/a/11772340/1219722 . Bu yüzden bu bir "karmaşıklık" değildir, çünkü bu şekilde beyan edilen POD'lar ağ kodunda gerçek hayat kodunda biraz kullanılır.
Dmitry Rubanovich

2

(Gcc bile derlemek olmadığından devre dışı optimizasyonlar ile belirli bir işlemci mimarisi üzerinde tek derleyici'nın çıkışı, That ++için addoptimize ederken hızlı ve kirli örnekte ), ima gibi görünüyor bu şekilde atomik olduğunu artırılmıyor (bu standart ile uyumlu olduğu anlamına gelmez erişmeye çalışırken tanımsız davranışa neden olur num, çünkü dizisindeki) ve her durumda yanlış addolduğunu değil x86 atomik.

Atomların ( lockkomut önekini kullanarak ) x86'da nispeten ağır olduğunu ( bu ilgili cevaba bakınız ), ancak yine de bu kullanım durumunda çok uygun olmayan bir muteksten önemli ölçüde daha az olduğunu unutmayın.

Aşağıdaki sonuçlar derlendiğinde clang ++ 3.8'den alınmıştır -Os.

Bir int değerini "normal" yolla artırarak:

void inc(int& x)
{
    ++x;
}

Bu şu şekilde derlenir:

inc(int&):
    incl    (%rdi)
    retq

Atomik yolla referansla geçirilen bir int değerini arttırmak:

#include <atomic>

void inc(std::atomic<int>& x)
{
    ++x;
}

Normal yoldan çok daha karmaşık olmayan bu örnek, sadece locköneki incltalimatlara eklenir - ancak daha önce belirtildiği gibi bunun ucuz olmadığı konusunda dikkatli olun . Montajın kısa görünmesi hızlı olduğu anlamına gelmez.

inc(std::atomic<int>&):
    lock            incl    (%rdi)
    retq

-2

Derleyiciniz artış için yalnızca tek bir talimat kullandığında ve makineniz tek iş parçacıklı olduğunda, kodunuz güvenlidir. ^^


-3

Aynı kodu x86 olmayan bir makinede derlemeyi deneyin, böylece çok farklı montaj sonuçları görürsünüz.

Bunun nedeni atomik num++ gibi görünüyor , çünkü x86 makinelerinde, 32 bitlik bir tamsayıyı arttırmak aslında atomiktir (bellek alımı gerçekleşmediği varsayılarak). Ancak bu, c ++ standardı tarafından garanti edilmez ve x86 komut kümesini kullanmayan bir makinede olması muhtemel değildir. Dolayısıyla bu kod, yarış koşullarından platformlar arası güvenli değildir.

Ayrıca x86 mimarisinde bile bu kodun Yarış Koşulları'ndan korunduğuna dair güçlü bir garantiniz yoktur, çünkü x86, özellikle belirtilmediği sürece yükler ve belleğe depolamaz. Bu nedenle, birden çok iş parçacığı bu değişkeni aynı anda güncellemeye çalıştıysa, önbelleğe alınmış (eski) değerleri arttırabilir

Öyleyse sahip olmamızın nedeni, std::atomic<int>temel hesaplamaların atomikliğinin garanti edilmediği bir mimariyle çalışırken, derleyiciyi atom kodu üretmeye zorlayacak bir mekanizmaya sahip olmanızdır.


"x86 makinelerinde 32 bitlik bir tamsayıyı artırmak aslında atomiktir." kanıtlayan belgelere link verebilir misiniz?
Slava

8
X86'da da atomik değil. Tek çekirdekli güvenli, ancak birden fazla çekirdek varsa (ve varsa) hiç atomik değildir.
harold

X86 addaslında atomik olarak garanti ediliyor mu? Kayıt artışları atomik olsaydı şaşırmazdım, ama bu pek kullanışlı değil; yazmaç artışını başka bir iş parçacığına görünür kılmak için, bellekte olması gerekir; bu, onu yüklemek ve saklamak için ek talimatlar gerektirir ve atomikliği giderir. Anladığım kadarıyla locktalimatlar için önek bu yüzden var; sadece kullanışlı atomik addkayıttan çıkarılmış hafızaya uygulanır lockve önbellek satırını işlem süresince kilitlendiğinden emin olmak için öneki kullanır .
ShadowRanger

@Slava @Harold @ShadowRanger Cevabı güncelledim. addAtomik, ama bunun kodun yarış koşulu için güvenli olduğu anlamına gelmediğini açıkça belirttim, çünkü değişiklikler küresel olarak hemen görünmez.
Xirema

3
@Xirema bunu tanımı gereği "atomik değil" yapar
harold
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.