यह वही है जो C ++ डेटा रेस के रूप में परिभाषित करता है जो कि अपरिभाषित व्यवहार का कारण बनता है, भले ही एक संकलक कोड का उत्पादन करने के लिए हुआ हो, जो आपने कुछ लक्ष्य मशीन पर आशा की थी। आपको std::atomic
विश्वसनीय परिणामों के लिए उपयोग करने की आवश्यकता है , लेकिन आप इसका उपयोग तब कर सकते हैं memory_order_relaxed
जब आपको पुन: व्यवस्थित करने की परवाह नहीं है। नीचे कुछ उदाहरण कोड और asm आउटपुट का उपयोग करके देखें fetch_add
।
लेकिन सबसे पहले, सवाल का विधानसभा भाषा हिस्सा:
चूंकि संख्या ++ एक निर्देश ( add dword [num], 1
) है, तो क्या हम यह निष्कर्ष निकाल सकते हैं कि इस मामले में संख्या ++ परमाणु है?
मेमोरी-डेस्टिनेशन निर्देश (प्योर स्टोर्स के अलावा) कई आंतरिक चरणों में होने वाले पठन-संशोधित-राइट-ऑपरेशन हैं । कोई वास्तुशिल्प रजिस्टर संशोधित नहीं किया गया है, लेकिन सीपीयू को आंतरिक रूप से डेटा को पकड़ना पड़ता है, जबकि वह इसे अपने ALU के माध्यम से भेजता है । वास्तविक रजिस्टर फ़ाइल केवल सबसे सरल सीपीयू के अंदर डेटा स्टोरेज का एक छोटा सा हिस्सा है, जिसमें एक चरण के आउटपुट को दूसरे चरण के इनपुट के रूप में रखने के साथ लैक्टेस होते हैं, आदि।
अन्य सीपीयू से मेमोरी ऑपरेशन लोड और स्टोर के बीच वैश्विक रूप से दृश्यमान हो सकते हैं। यानी add dword [num], 1
लूप में चलने वाले दो धागे एक-दूसरे के स्टोर पर कदम रखेंगे। ( अच्छा चित्र के लिए @ मार्गरेट का जवाब देखें)। प्रत्येक दो थ्रेड्स से 40k वेतन वृद्धि के बाद, काउंटर केवल वास्तविक मल्टी-कोर x86 हार्डवेयर पर ~ 60k (80k नहीं) तक चला गया हो सकता है।
"परमाणु", ग्रीक शब्द से जिसका अर्थ अविभाज्य है, का अर्थ है कि कोई भी पर्यवेक्षक ऑपरेशन को अलग-अलग चरणों के रूप में नहीं देख सकता है । सभी बिट्स के लिए एक साथ शारीरिक / विद्युत रूप से तुरंत प्राप्त करना एक भार या स्टोर के लिए इसे प्राप्त करने का सिर्फ एक तरीका है, लेकिन यह ALU ऑपरेशन के लिए भी संभव नहीं है। मैं x86 पर Atomicity के अपने उत्तर में शुद्ध भार और शुद्ध दुकानों के बारे में बहुत अधिक विस्तार से गया था , जबकि यह उत्तर पढ़ने-संशोधित-लिखने पर केंद्रित है।
lock
उपसर्ग पूरे आपरेशन प्रणाली में हर संभव पर्यवेक्षकों के संबंध में परमाणु बनाने के लिए कई पढ़ने-लिखने की संशोधित (स्मृति गंतव्य) निर्देश के लिए लागू किया जा सकता है (अन्य कोर और डीएमए उपकरणों, नहीं एक आस्टसीलस्कप सीपीयू पिन को झुका)। इसलिए यह मौजूद है। ( यह प्रश्नोत्तर भी देखें )।
तो lock add dword [num], 1
है परमाणु । एक सीपीयू कोर जो यह निर्देश देता है कि निर्देश कैश स्थिति को अपने निजी L1 कैश में तब संशोधित स्थिति में रखेगा जब लोड कैश से डेटा पढ़ता है जब तक कि स्टोर अपना परिणाम कैश में वापस नहीं करता है। यह MESI कैश सुसंगतता प्रोटोकॉल (या मल्टी-कोर AMD AMD द्वारा उपयोग किए गए इसके MOESI / MESIF संस्करण) के नियमों के अनुसार सिस्टम में किसी भी अन्य कैश को लोड से स्टोर करने के लिए कैश लाइन की एक प्रति होने से रोकता है। इंटेल सीपीयू, क्रमशः)। इस प्रकार, अन्य कोर द्वारा ऑपरेशन या तो पहले या बाद में होते हैं, न कि दौरान।
lock
उपसर्ग के बिना , एक और कोर कैश लाइन का स्वामित्व ले सकता है और इसे हमारे लोड के बाद लेकिन हमारे स्टोर से पहले संशोधित कर सकता है, ताकि हमारे स्टोर और स्टोर के बीच अन्य स्टोर विश्व स्तर पर दिखाई दे। कई अन्य उत्तरों से यह गलत हो जाता है, और दावा करते हैं कि आपके बिना lock
एक ही कैश लाइन की परस्पर विरोधी प्रतियाँ प्राप्त होंगी। यह सुसंगत कैश के साथ एक प्रणाली में कभी नहीं हो सकता है।
(यदि कोई lock
निर्देश दो मेमोरी लाइनों पर फैला हुआ है, जो मेमोरी पर काम करता है, तो यह सुनिश्चित करने के लिए बहुत अधिक कार्य लेता है कि ऑब्जेक्ट के दोनों हिस्सों में परिवर्तन परमाणु बने रहें क्योंकि वे सभी पर्यवेक्षकों को प्रचारित करते हैं, इसलिए कोई पर्यवेक्षक फाड़ नहीं सकता। सीपीयू हो सकता है। जब तक डेटा मेमोरी नहीं मारता तब तक पूरी मेमोरी बस को लॉक करना है। अपने एटॉमिक वेरिएबल्स को मिसअलाइन न करें!
ध्यान दें कि lock
उपसर्ग भी एक निर्देश को पूर्ण मेमोरी बैरियर (जैसे कि MFENCE ) में बदल देता है , सभी रन-टाइम री-मोडिंग को रोक देता है और इस प्रकार अनुक्रमिक स्थिरता देता है। (देखें जेफ प्रेशिंग की उत्कृष्ट ब्लॉग पोस्ट । उनकी अन्य पोस्ट्स भी उत्कृष्ट हैं, और स्पष्ट रूप से लॉक-फ्री प्रोग्रामिंग के बारे में बहुत सारी चीजें बताती हैं , जो x86 और अन्य हार्डवेयर विवरणों से लेकर C ++ नियमों तक हैं।)
एक यूनिप्रोसेसर मशीन पर, या एकल-थ्रेडेड प्रक्रिया में , एक एकल RMW निर्देश वास्तव में एक lock
उपसर्ग के बिना परमाणु है । अन्य कोड के लिए साझा चर तक पहुंचने का एकमात्र तरीका सीपीयू के लिए एक संदर्भ स्विच करना है, जो एक निर्देश के बीच में नहीं हो सकता है। तो एक प्लेन dec dword [num]
सिंगल-थ्रेडेड प्रोग्राम और उसके सिग्नल हैंडलर के बीच या सिंगल-कोर मशीन पर चलने वाले मल्टी थ्रेडेड प्रोग्राम के बीच सिंक्रोनाइज़ कर सकता है। एक अन्य प्रश्न पर मेरे उत्तर के दूसरे भाग को देखें , और इसके तहत टिप्पणियां, जहां मैं इसे और अधिक विस्तार से समझाता हूं।
C ++ पर वापस:
num++
कंपाइलर को बताए बिना उपयोग करने के लिए यह पूरी तरह से फर्जी है कि आपको इसे एक ही रीड-मॉडिफाई-राइट इंप्लीमेंट के संकलन के लिए चाहिए:
;; Valid compiler output for num++
mov eax, [num]
inc eax
mov [num], eax
यदि आप num
बाद के मूल्य का उपयोग करते हैं तो यह बहुत संभावना है : संकलक वृद्धि के बाद इसे एक रजिस्टर में लाइव रखेगा। इसलिए भले ही आप यह जांच लें कि num++
अपने आप कैसे संकलित किया जाता है, आसपास के कोड को बदलने से यह प्रभावित हो सकता है।
(यदि बाद में मूल्य की आवश्यकता नहीं है, inc dword [num]
तो प्राथमिकता दी जाती है; आधुनिक x86 सीपीयू तीन अलग-अलग निर्देशों का उपयोग करते हुए कम से कम कुशलता से एक मेमोरी-गंतव्य आरएमडब्ल्यू निर्देश चलाएगा। मजेदार तथ्य:gcc -O3 -m32 -mtune=i586
वास्तव में यह उत्सर्जन करेगा , क्योंकि (पेंटियम) पी 5 के सुपरस्कूलर पाइपलाइन से जमा हुआ है। जिस तरह से पी 6 और बाद में माइक्रोआर्किटेक्चर्स कई सरल माइक्रो-ऑपरेशंस के लिए जटिल निर्देश को डिकोड करते हैं। अधिक जानकारी के लिए एग्नर फॉग के इंस्ट्रक्शन टेबल / माइक्रोआर्किटेक्चर गाइड देखें।86 कई उपयोगी लिंक के लिए टैग विकी (इंटेल के x86 ISA मैनुअल सहित, जो स्वतंत्र रूप से पीडीएफ के रूप में उपलब्ध हैं)।
C ++ मेमोरी मॉडल के साथ लक्ष्य मेमोरी मॉडल (x86) को भ्रमित न करें
संकलन-समय पुन: व्यवस्थित करने की अनुमति है । आपको std के साथ जो मिलता है उसका दूसरा भाग :: परमाणु संकलन समय-सीमा के नियंत्रण पर है, यह सुनिश्चित करने के लिए कि आपकाnum++
कुछ अन्य ऑपरेशन के बाद ही आप विश्व स्तर पर दिखाई देते हैं।
क्लासिक उदाहरण: किसी डेटा को किसी अन्य थ्रेड को देखने के लिए बफ़र में संग्रहीत करना, फिर एक ध्वज सेट करना। भले ही x86 लोड / रिलीज स्टोर्स को मुफ्त में अधिग्रहित करता है, फिर भी आपको कंपाइलर को उपयोग करके पुन: व्यवस्थित नहीं करना है flag.store(1, std::memory_order_release);
।
आप उम्मीद कर रहे होंगे कि यह कोड अन्य थ्रेड्स के साथ सिंक्रनाइज़ होगा:
// 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++;
लेकिन यह नहीं होगा। संकलक flag++
फ़ंक्शन कॉल में स्थानांतरित करने के लिए स्वतंत्र है (यदि यह फ़ंक्शन को बताता है या जानता है कि यह नहीं दिखता है flag
)। तब यह पूरी तरह से संशोधन को दूर कर सकता है, क्योंकि flag
यह भी नहीं है volatile
। (और नहीं, सी ++ volatile
एसटीडी के लिए एक उपयोगी विकल्प नहीं है :: परमाणु। एसटीडी :: परमाणु कंपाइलर का मानना है कि स्मृति में मूल्यों को एसिंक्रोनस रूप से समान रूप से संशोधित किया जा सकता है volatile
, लेकिन इसके अलावा भी बहुत कुछ है।volatile std::atomic<int> foo
यह नहीं है। जैसा कि std::atomic<int> foo
@Richard Hodges के साथ चर्चा की गई है।)
अपरिभाषित व्यवहार के रूप में गैर-परमाणु चर पर डेटा की दौड़ को परिभाषित करना वह है जो कंपाइलर को लूप से लोड और सिंक स्टोर करने देता है, और मेमोरी के लिए कई अन्य अनुकूलन जो कि कई थ्रेड्स का संदर्भ हो सकता है। ( यूएलबी कंपाइल ऑप्टिमाइज़ेशन सक्षम करने के बारे में अधिक जानने के लिए इस LLVM ब्लॉग को देखें ।)
जैसा कि मैंने उल्लेख किया है, x86 lock
उपसर्ग एक पूर्ण मेमोरी बाधा है, इसलिए num.fetch_add(1, std::memory_order_relaxed);
x86 पर समान कोड का उपयोग करना num++
(डिफ़ॉल्ट क्रमिक स्थिरता है), लेकिन यह अन्य आर्किटेक्चर (जैसे एआरएम) पर बहुत अधिक कुशल हो सकता है। यहां तक कि x86 पर, आराम से अधिक संकलन-समय पुन: व्यवस्थित करने की अनुमति मिलती है।
यह वही है जो जीसीसी वास्तव में x86 पर करता है, कुछ कार्यों के लिए जो एक std::atomic
वैश्विक चर पर काम करते हैं ।
Godbolt संकलक एक्सप्लोरर पर अच्छी तरह से स्वरूपित स्रोत + विधानसभा भाषा कोड देखें । आप एआरएम, एमआइपीएस और पावरपीसी सहित अन्य लक्ष्य आर्किटेक्चर का चयन कर सकते हैं, यह देखने के लिए कि उन लक्ष्यों के लिए एटोमिक्स से आपको किस प्रकार की विधानसभा भाषा कोड मिलती है।
#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
ध्यान दें कि अनुक्रमिक-संगति स्टोर के बाद MFENCE (एक पूर्ण अवरोध) की आवश्यकता कैसे होती है। x86 को सामान्य रूप से दृढ़ता से आदेश दिया जाता है, लेकिन स्टोरलॉड रीऑर्डरिंग की अनुमति है। एक पिपेलिनेटेड आउट-ऑफ-ऑर्डर सीपीयू पर अच्छे प्रदर्शन के लिए स्टोर बफर होना आवश्यक है। एक्ट में पकड़े गए जेफ प्रेशिंग की मेमोरी रीऑर्डरिंग, एमएफईएनईईसी का उपयोग नहीं करने के परिणामों को दिखाती है, वास्तविक कोड के साथ वास्तविक हार्डवेयर पर घटित होने को दिखाने के लिए।
पुन :: @ विलय के बारे में टिप्पणी में चर्चा Hodges जवाब के बारे में संकलक एसटीजी :: परमाणु num++; num-=2;
संचालन एक num--;
निर्देश में : :
इसी विषय पर एक अलग प्रश्नोत्तर: कंपाउंड रिड्यूसेंट std मर्ज क्यों नहीं करते : परमाणु लिखते हैं? , जहाँ मेरा उत्तर मेरे लिखे हुए चीज़ों को बहुत ही आराम देता है।
वर्तमान संकलक वास्तव में ऐसा नहीं करते हैं (अभी तक), लेकिन इसलिए नहीं कि उन्हें अनुमति नहीं है। C ++ WG21 / P0062R1: कंपाइलरों को एटॉमिक्स का अनुकूलन कब करना चाहिए? इस अपेक्षा पर चर्चा करता है कि कई प्रोग्रामर के पास यह है कि कंपाइलर "आश्चर्यजनक" अनुकूलन नहीं करेंगे, और मानक प्रोग्रामर को नियंत्रण देने के लिए क्या कर सकते हैं। N4455 उन चीजों के कई उदाहरणों पर चर्चा करता है जिन्हें इस एक सहित अनुकूलित किया जा सकता है। यह बताता है कि इनलाइनिंग और निरंतर-प्रसार ऐसी चीजों को पेश कर सकते हैं, fetch_or(0)
जो मूल में बदलने में सक्षम हो सकती हैं load()
(लेकिन अभी भी अधिग्रहित और जारी करना है), तब भी जब मूल स्रोत में कोई स्पष्ट रूप से अनावश्यक परमाणु ऑप्स नहीं थे।
वास्तविक कारण संकलक ऐसा नहीं करते (अभी तक) हैं: (1) किसी ने जटिल कोड नहीं लिखा है जो संकलक को सुरक्षित रूप से (कभी भी गलत हो रहा है) ऐसा करने की अनुमति देगा, और (2) यह संभवतः कम से कम के सिद्धांत का उल्लंघन करता है आश्चर्य है । पहली जगह में सही ढंग से लिखने के लिए लॉक-फ्री कोड पर्याप्त कठिन है। तो परमाणु हथियारों के आपके उपयोग में आकस्मिक मत बनो: वे सस्ते नहीं हैं और बहुत अनुकूलन नहीं करते हैं। यह हमेशा आसान नहीं होता है std::shared_ptr<T>
, क्योंकि इसके साथ कोई गैर-परमाणु संस्करण नहीं होता है, हालाँकि, इसका कोई गैर-परमाणु संस्करण नहीं है (हालाँकि यहाँ एक उत्तरshared_ptr_unsynchronized<T>
gcc को परिभाषित करने का आसान तरीका है )।
num++; num-=2;
संकलन करने के लिए वापस आ रहे हैं जैसे कि यह था num--
: कंपाइलरों को ऐसा करने की अनुमति है, जब तक कि num
यह न हो volatile std::atomic<int>
। यदि एक पुनरावृत्ति संभव है, तो जैसा कि नियम कंपाइलर को संकलन समय पर निर्णय लेने की अनुमति देता है कि यह हमेशा उस तरह से होता है। कुछ भी गारंटी नहीं है कि एक पर्यवेक्षक मध्यवर्ती मूल्यों ( num++
परिणाम) को देख सकता है ।
Ie अगर ऑर्डरिंग जहां इन ऑपरेशंस के बीच विश्व स्तर पर कुछ भी दिखाई नहीं देता है, तो स्रोत की ऑर्डरिंग आवश्यकताओं (एब्सट्रैक्ट मशीन के लिए C ++ नियमों के अनुसार, लक्ष्य आर्किटेक्चर के अनुसार नहीं) के अनुरूप है, कंपाइलर / के lock dec dword [num]
बजाय एक भी उत्सर्जन कर सकता है ।lock inc dword [num]
lock sub dword [num], 2
num++; num--
गायब नहीं हो सकता है, क्योंकि यह अभी भी अन्य धागे के साथ संबंध के साथ एक सिंक्रनाइज़ेशन है जो दिखता है num
, और यह दोनों अधिग्रहण-लोड और एक रिलीज-स्टोर है जो इस धागे में अन्य संचालन के पुन: संचालन को अस्वीकार करता है। X86 के लिए, यह एक lock add dword [num], 0
(यानी num += 0
) के बजाय एक MFENCE को संकलित करने में सक्षम हो सकता है ।
जैसा कि PR0062 में चर्चा की गई है , संकलन समय पर गैर-आसन्न परमाणु ऑप्स का अधिक आक्रामक विलय बुरा हो सकता है (जैसे एक प्रगति काउंटर केवल हर पुनरावृत्ति के बजाय एक बार अपडेट हो जाता है), लेकिन यह डाउनसाइड्स के बिना प्रदर्शन में भी मदद कर सकता है (जैसे स्किपिंग) जब परमाणु की एक प्रति shared_ptr
बनाई जाती है और नष्ट हो जाती है, तो रेफरी का परमाणु inc / dec मायने रखता है, यदि संकलक यह साबित कर सकता है कि shared_ptr
अस्थायी के पूरे जीवनकाल के लिए एक और वस्तु मौजूद है।)
यहां तक कि num++; num--
विलय एक लॉक कार्यान्वयन की निष्पक्षता को चोट पहुंचा सकता है जब एक धागा अनलॉक होता है और तुरंत लॉक हो जाता है। अगर यह वास्तव में कभी भी asm में रिलीज़ नहीं होता है, तो भी हार्डवेयर आर्बिट्रेशन मैकेनिज़्म उस बिंदु पर लॉक को हथियाने का एक और मौका नहीं देगा।
वर्तमान gcc6.2 और clang3.9 के साथ, आपको अभी lock
भी memory_order_relaxed
सबसे स्पष्ट रूप से अनुकूलन योग्य मामले में भी अलग-अलग एड ऑपरेशन मिलते हैं । ( गॉडबोल्ट कंपाइलर एक्सप्लोरर ताकि आप देख सकें कि नवीनतम संस्करण अलग हैं।)
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
परमाणु है?