कोई एक अकेला निर्देश देखने में एक अविभाज्य काम लग सकता है, जबकि असल में वह कुछ और ही होता है। मान लीजिए, मेमोरी में रखी किसी वैल्यू में पाँच जोड़ना:
add qword [rel counter], 5
अंदर से देखें तो प्रोसेसर के पास मेमोरी में सीधे जोड़ने का कोई तरीका नहीं होता। वह इस एक निर्देश को तीन छोटे चरणों में तोड़ देता है, जिन्हें माइक्रो-ऑपरेशन कहते हैं:
mov rax, qword [rel counter] ; 1. load the current value
add rax, 5 ; 2. add five
mov qword [rel counter], rax ; 3. store the result back
यह एक आम पैटर्न है जिसे पढ़ना-बदलना-लिखना (RMW) कहते हैं।
ध्यान दीजिए कि पढ़ना (या लोड) और लिखना (या स्टोर) अलग-अलग घटनाएँ हैं, इसलिए उनके बीच समय का एक अंतराल रहता है।
हमें यह आम तौर पर दिखता नहीं, क्योंकि एकल कोर पर हर निर्देश का पूरा असर अगले निर्देश से पहले सुनिश्चित हो जाता है।
इसलिए वह अंतराल दिखता नहीं, और add एक ही इकाई की तरह व्यवहार करता है।
लेकिन आधुनिक CPU शायद ही कभी एकल कोर वाले होते हैं, और ऐप्लिकेशन अक्सर एक साथ कई कोरों पर चलते हैं, जिनके बीच कोई तय क्रम नहीं होता।
जब कई कोर बिल्कुल एक ही पल चल रहे हों, तो कोई दूसरा कोर उस अंतराल के अंदर, यानी इस कोर के लोड के बाद और उसके स्टोर से पहले, counter को पढ़ या लिख सकता है।
दो थ्रेड दोनों वही पुरानी वैल्यू लोड करते हैं, दोनों उसमें पाँच जोड़ते हैं, और दोनों अपना नतीजा स्टोर कर देते हैं।
दो बार जोड़ हुआ, लेकिन वैल्यू सिर्फ पाँच ही बढ़ी।
एक अपडेट चुपचाप खो गया।
यह एक डेटा रेस है, और मल्टी-थ्रेडेड कोड में यह एक आम समस्या है।
हर वैल्यू इस तरह खतरे में नहीं होती।
हर थ्रेड के अपने रजिस्टर और अपना स्टैक होता है, इसलिए रजिस्टर में रखी वैल्यू या थ्रेड के स्टैक पर रखा लोकल वेरिएबल सिर्फ उसी थ्रेड का होता है और रेस में नहीं पड़ सकता।
सिर्फ वही मेमोरी सुरक्षा की ज़रूरत रखती है जिसे थ्रेड आपस में साझा करते हैं, जैसे ऊपर counter।
x86-64 इस समस्या को हल करने के लिए कुछ निर्देश देता है, जो किसी निर्देश को अविभाज्य बना देते हैं। वह न सिर्फ उस कोर के लिए एक ही इकाई की तरह व्यवहार करता है जो उसे चला रहा है, बल्कि हर दूसरे कोर के लिए भी। जो काम एक साथ जुड़ा रहता है, जिसे कोई दूसरा कोर बीच से नहीं तोड़ सकता, उसे अटॉमिक कहते हैं।
कई कोरों पर चलने वाले ऐप्लिकेशन को मल्टी-थ्रेडेड कहना आम है। लेकिन थ्रेड और कोर एक चीज़ नहीं हैं। दो थ्रेड एक ही कोर पर एक साथ, बारी-बारी से चल सकते हैं, या अलग-अलग कोरों पर समानांतर में।
अगर पढ़ना-बदलना-लिखना कई निर्देशों में टूटा हो, तो बारी-बारी से चलने वाले थ्रेड उसमें पहले से ही रेस कर सकते हैं, क्योंकि OS किन्हीं भी दो निर्देशों के बीच थ्रेड बदल सकता है। लेकिन एक ही निर्देश के अंदर का अंतराल सिर्फ सचमुच समानांतर चलने वाले कोड में ही सामने आता है। चूँकि OS थ्रेड सिर्फ निर्देशों के बीच बदलता है, किसी एक निर्देश के अंदर कभी नहीं, इसलिए एकल कोर पर एक अकेला निर्देश अपने आप सुरक्षित रहता है।
कई कोरों में सचमुच अटॉमिक होना ही वह है जो नीचे दिए गए निर्देश देते हैं।
xchg निर्देश दो ऑपरेंड की अदला-बदली करता है।
डेस्टिनेशन ऑपरेंड, सोर्स ऑपरेंड में पहले से मौजूद वैल्यू के बराबर हो जाता है, और सोर्स ऑपरेंड, डेस्टिनेशन ऑपरेंड में पहले से मौजूद वैल्यू के बराबर हो जाता है।
सोचने में इसे ऐसे समझा जा सकता है मानो दो mov निर्देश एक साथ हो रहे हों।
हमेशा की तरह, इसे दो रजिस्टर ऑपरेंड के साथ, या एक मेमोरी ऑपरेंड और एक रजिस्टर ऑपरेंड के साथ इस्तेमाल किया जा सकता है:
mov eax, 1
xchg dword [rdi], eax ; [rdi] = 1, eax = the old value of [rdi]
mov ecx, 2
mov edx, 3
xchg edx, ecx ; edx = 2, ecx = 3
जब इसे मेमोरी ऑपरेंड के साथ इस्तेमाल किया जाता है, तो xchg हमेशा अटॉमिक होता है।
जब ऑपरेंड में से एक मेमोरी लोकेशन हो, तो xchg अपने आप अटॉमिक होता है।
इसका यह भी मतलब है कि ऐसी स्थिति में यह काम बहुत धीमा होता है।
अगर आपको अटॉमिक होने की ज़रूरत नहीं है, तो अदला-बदली किसी खाली रजिस्टर के ज़रिए सादे mov निर्देशों से कीजिए।
x86-64 में किसी निर्देश को अटॉमिक बनाने का सबसे आम तरीका है, उसमें lock प्रीफिक्स जोड़ना।
यह पढ़ने, बदलने और लिखने को एक अविभाज्य चरण में मिला देता है।
इसका मतलब है कि वह कोर पूरी प्रक्रिया के दौरान उस मेमोरी पर अपना एकाधिकार बनाए रखता है, इसलिए बीच में कोई दूसरा कोर उस लोकेशन को पढ़ या लिख नहीं सकता।
lock add qword [rel counter], 5 ; the read, the modify, and the write are one step
lock सिर्फ तब काम करता है जब डेस्टिनेशन मेमोरी हो, और सिर्फ उन्हीं निर्देशों पर जो उस मेमोरी को पढ़ना-बदलना-लिखना करते हैं:
add, sub, inc, dec, neg;and, or, xor, not;bts, btr, btc;xadd और cmpxchg, जिनके बारे में नीचे बताया गया है।किसी लोकेशन पर एकाधिकार रखना और हर दूसरे कोर को बाहर रोकना मुफ्त नहीं है।
lock प्रीफिक्स वाला काम अपने सादे रूप से काफी धीमा होता है, और जब कई कोर एक ही लोकेशन के लिए होड़ करते हैं तो और भी धीमा।
यह प्रीफिक्स सिर्फ उसी मेमोरी के लिए रखिए जिसे एक से अधिक थ्रेड के बदलने की उम्मीद हो। अगर मेमोरी साझा नहीं है या उसे सिर्फ पढ़ा ही जाता है, तो इसका इस्तेमाल न कीजिए।
सादा lock add मेमोरी को अपडेट तो करता है, लेकिन पुरानी वैल्यू फेंक देता है।
अक्सर पुरानी वैल्यू ही चाहिए होती है, मसलन हर थ्रेड को एक अलग टिकट नंबर देने के लिए।
xadd निर्देश (x यानी exchange) जोड़ते हुए पिछली वैल्यू लौटा देता है।
यह जोड़ को डेस्टिनेशन में लिख देता है और डेस्टिनेशन की मूल वैल्यू सोर्स रजिस्टर में छोड़ देता है।
mov rax, 1
lock xadd qword [rdi], rax ; [rdi] = [rdi] + rax = [rdi] + 1
; rax = the old value of [rdi]
lock प्रीफिक्स के साथ यह एक अटॉमिक फेच-एंड-ऐड है।
जब कई थ्रेड इसे एक ही काउंटर पर चलाते हैं, तो हर कॉल एक अलग पुरानी वैल्यू लौटाती है।
lock add की तरह, काउंटर ठीक उतनी ही संख्या पर पहुँचता है जितनी बार कॉल हुई।
लेकिन lock add से अलग, यहाँ बीच की हर वैल्यू भी लौटाई जाती है, हर कॉलर को एक।
xadd जोड़ता है और xchg ऊपर से लिख देता है, लेकिन इनमें से कोई भी नई वैल्यू को मौजूदा वैल्यू पर निर्भर नहीं बना सकता, और न ही उसे सिर्फ तभी लागू कर सकता है जब नीचे कुछ न बदला हो।
यही सशर्त अपडेट cmpxchg, यानी तुलना और एक्सचेंज, देता है, और यह इन सबमें सबसे सामान्य प्रिमिटिव है।
cmpxchg dest, src rax को एक अंतर्निहित एक्युमुलेटर की तरह इस्तेमाल करता है और उसकी dest से तुलना करता है:
dest == rax, तो dest = src और ZF = 1।dest != rax, तो rax = dest और ZF = 0।ध्यान दीजिए कि dest तभी अपडेट होता है जब वह उस अपेक्षित वैल्यू के बराबर हो जो पहले rax में लोड की गई थी।
यह बराबरी पक्का करती है कि dest में अब भी वही वैल्यू है जिससे नई वैल्यू निकाली गई थी, इसलिए पुरानी पढ़ी गई वैल्यू पर आधारित अपडेट कभी लागू नहीं होता।
इसी वजह से cmpxchg अटॉमिक अपडेट की बुनियादी ईंट है, जिसे कंपेयर-एंड-स्वैप (CAS) भी कहते हैं:
mov rax, qword [rdi] ; rax = the value we expect to find
.retry:
lea rcx, [rax + 10] ; rcx = the new value we want to install
lock cmpxchg qword [rdi], rcx ; if [rdi] still equals rax, store rcx and set ZF
; otherwise reload rax with the current value, clear ZF
jnz .retry ; ZF is cleared, so another thread won the race. Recompute and retry
यह रीट्राई लूप लॉक-फ्री अपडेट का दिल है।
पढ़ने और तुलना-और-एक्सचेंज के बीच का अंतराल ठीक वही समय होता है जब कोई दूसरा थ्रेड बीच में आ सकता है। cmpxchg इसे यह करके पकड़ लेता है: वह पुरानी पढ़ी गई वैल्यू से निकाली गई वैल्यू को स्टोर करने से इनकार कर देता है।
अब तक हर काम एक ही लोकेशन पर हुआ है। जब थ्रेड एक से अधिक लोकेशन के ज़रिए तालमेल बिठाते हैं, तो एक नया सवाल उठता है: एक थ्रेड की लिखी वैल्यू दूसरे थ्रेड को किस क्रम में दिखती हैं। इस सवाल का जवाब प्रोसेसर की मेमोरी ऑर्डरिंग में है।
ब्रांचलेस-कोड कॉन्सेप्ट में यह बात पहले आ चुकी है कि आधुनिक कोर निर्देशों को एक-एक करके धीरे-धीरे नहीं निपटाता। वह एक साथ कई निर्देश चलता रहता है और जहाँ मौका मिले आगे बढ़ जाता है। इसका मतलब है कि कोई लिखावट दूसरे कोरों को प्रोग्राम के बताए क्रम से देर से दिख सकती है, जबकि उसके बाद के निर्देश पहले ही आगे बढ़ चुके होते हैं।
x86-64 सादे लोड और स्टोर के बीच मज़बूत मेमोरी ऑर्डरिंग बनाए रखता है, जिससे हर कोर पर:
सिर्फ एक ही क्रम-बदलाव संभव है: किसी स्टोर का, किसी अलग पते के बाद वाले लोड के बाद पूरा होता दिखना।
lock प्रीफिक्स वाला निर्देश, या मेमोरी ऑपरेंड वाला xchg, एक पूरा बैरियर है: इसके आर-पार किसी भी दिशा में कुछ खिसकता हुआ नहीं दिखता।
इसी वजह से ज़्यादातर हालात में पूरा क्रम सुनिश्चित करने के लिए यही काफी होता है।
pause
जो निर्देश एक फ्लैग सेट करते हुए उसकी पिछली स्थिति भी लौटाते हैं, उन्हें टेस्ट-एंड-सेट कहते हैं। इन्हें एक स्पिनलॉक की बुनियाद के तौर पर इस्तेमाल किया जा सकता है, जो पक्का करता है कि किसी कोर के पास कोड के किसी हिस्से तक सिर्फ उसी की पहुँच हो।
यह पूरा एल्गोरिदम है, जो एक बाइनरी फ्लैग के साथ xchg निर्देश का इस्तेमाल करता है:
0 रहता है।1 के साथ अदला-बदली करता है।1 है, तो इसका मतलब है कि लॉक किसी दूसरे कोर के पास है।
तब यह कोर इंतज़ार करता है और लॉक दोबारा पाने की कोशिश करता है।0 है, तो इसका मतलब है कि लॉक खाली था।
xchg ने अब उसे 1 कर दिया है, और बाकी कोर तब तक इंतज़ार करेंगे जब तक यह कोर उसे छोड़ न दे।0 कर देना लॉक छोड़ देता है।acquire:
mov eax, 1
xchg dword [rdi], eax ; try to take the lock; eax = its old value
test eax, eax
jnz .held ; old value was 1: someone else holds it
ret ; old value was 0: the lock is ours
.held:
pause ; wait before trying again
jmp acquire
इंतज़ार वाले लूप में pause निर्देश इससे नहीं बदलता कि कोड क्या निकालता है।
यह प्रोसेसर को संकेत देता है कि यह एक स्पिन-वेट है।
तब CPU इंतज़ार कर रहे थ्रेड की बिजली की खपत घटा सकता है और उसी कोर पर चल रहे दूसरे थ्रेड को मौका दे सकता है।
pause के बिना भी स्पिन लूप सही रहता है, बस फालतू है।
जब थ्रेड अपना काम पूरा कर लेता है, तो वह mov से सादा 0 स्टोर करके लॉक छोड़ सकता है।
mov से ज़्यादा कुछ नहीं चाहिए, क्योंकि x86-64 पर लोड और स्टोर को उनके बाद के स्टोर के पीछे नहीं सरकाया जाता।
जो स्टोर अपने से पहले के एक्सेसों से कभी आगे नहीं निकलता, उसके बारे में कहा जाता है कि उसमें रिलीज़ ऑर्डरिंग है, और x86 पर हर सादे स्टोर में यह होती है।
कोई भी निर्देश जो किसी मेमोरी लोकेशन को अटॉमिक तरीके से जाँचकर सेट करता है, स्पिनलॉक के लिए इस्तेमाल किया जा सकता है।
उदाहरण के लिए, xchg की जगह lock bts इस्तेमाल किया जा सकता है, जो किसी खास बिट को सेट करते हुए यह भी जाँच लेता है कि वह पहले से सेट थी या नहीं।
ध्यान दीजिए कि फ्लैग, जैसे bts द्वारा बदला जाने वाला CF, rflags नाम के रजिस्टर का हिस्सा होते हैं।
इसका मतलब है कि वे सिर्फ हर थ्रेड के अपने होते हैं।
Grand Marquee में पहली रात है, और सारे कर्मचारी एक साथ काम कर रहे हैं। भीड़ हर दरवाज़े से अंदर उमड़ रही है, टिकट की कई खिड़कियाँ एक ही टिकट रोल से टिकट बेच रही हैं, प्रीमियर के टिकट लगभग पूरे हो चुके हैं, और ऊपर प्रोजेक्शन बूथ में रीलें चलती रहनी चाहिए।
कर्मचारी जो कुछ भी साझा करते हैं, वह सब एक मेमोरी एड्रेस पर रहता है, और टेस्ट आपके फंक्शन एक ही समय में कई थ्रेड्स से कॉल करते हैं। जो वैल्यू अलग-अलग चरणों में पढ़ी जाती है, बदली जाती है और वापस लिखी जाती है, उसके अपडेट इतने लोड में खो जाएँगे, इसलिए हर फंक्शन को अपना अपडेट एटॉमिक बनाना होगा।
इस अभ्यास में साझा की गई सारी वैल्यू 64-बिट साइन्ड पूर्णांक हैं।
आपके सामने पाँच काम हैं।
इस अभ्यास में अपडेट एटॉमिक निर्देशों से करने चाहिए। टेस्ट आपके फंक्शन एक साथ, कई थ्रेड्स से चलाते हैं और जाँचते हैं कि कोई अपडेट खो न जाए, इसलिए बिना एटॉमिक पढ़ने-बदलने-लिखने वाला कोड उन्हें बहुत संभवतः फेल कर देगा।
सारे दरवाज़े खुल चुके हैं, और हर दरवाज़े पर एक कर्मचारी टिकट फाड़ रहा है। अंदर जाने दिए गए हर समूह की गिनती एक ही साझा प्रवेश-गिनती में होती है, और जब हर दरवाज़ा एक ही समय गिन रहा हो, तब भी हॉल की कुल गिनती सही निकलनी चाहिए।
admit_group फंक्शन बनाइए, जो किसी समूह के आकार को एटॉमिक तरीके से गिनती में जोड़ता है।
यह फंक्शन इस क्रम में ये आर्गुमेंट लेता है:
admissions: साझा प्रवेश-गिनती का मेमोरी एड्रेस, एक 64-बिट साइन्ड पूर्णांक।group_size: समूह में लोगों की संख्या, एक 64-बिट साइन्ड पूर्णांक, जो हमेशा 0 से बड़ा होता है।admissions = 120, group_size = 4
// admissions is now 124
टेस्ट एक ही समय में कई दरवाज़ों से समूहों को अंदर जाने देते हैं। अंत में गिनती उन सभी समूहों के जोड़ के बराबर होनी चाहिए जिन्हें अंदर जाने दिया गया।
इस फंक्शन का कोई रिटर्न वैल्यू नहीं है। गिनती उसी जगह अपडेट की जानी चाहिए।
प्रोजेक्टर में एक समय में सिर्फ़ एक रील लगी रहती है। अगली रील लगाना और चल रही रील निकालना एक ही काम होना चाहिए: मशीन पर दो लोग काम कर रहे हों और ये दो अलग-अलग चरण बन जाएँ, तो हो सकता है वही रील दोनों को मिल जाए, या कोई रील फर्श पर गिर जाए।
change_reel फंक्शन बनाइए, जो एटॉमिक तरीके से अगली रील लगाता है और पहले लगी रील को लौटाता है।
यह फंक्शन इस क्रम में ये आर्गुमेंट लेता है:
mounted: लगी हुई रील की पहचान संख्या का मेमोरी एड्रेस, एक 64-बिट साइन्ड पूर्णांक।next_reel: लगाने वाली रील की पहचान संख्या, एक 64-बिट साइन्ड पूर्णांक।mounted = 7, next_reel = 9
// => 7, and mounted is now 9
यह फंक्शन पहले लगी रील की पहचान संख्या एक 64-बिट साइन्ड पूर्णांक के रूप में लौटाता है। टेस्ट एक ही माउंट पर, एक ही समय, कई थ्रेड्स से रीलें बदलवाते हैं और जाँचते हैं कि जो भी रील अंदर दी जाती है, वह ठीक एक बार बाहर निकलती है।
हर खिड़की बिक चुके टिकटों की एक ही साझा गिनती से टिकट बेचती है। यहाँ सिर्फ़ गिनती बढ़ा देना काफी नहीं है, जैसा दरवाज़ों पर चलता है: हर बिक्री को यह भी मालूम होना चाहिए कि उसे कौन-सा नंबर मिला। किन्हीं दो बिक्री का नंबर एक जैसा नहीं हो सकता, और भीड़ खत्म होने के बाद गिनती बिके टिकटों की संख्या के बिल्कुल बराबर होनी चाहिए।
sell_ticket फंक्शन बनाइए, जो एटॉमिक तरीके से गिनती में एक जोड़ता है और अभी बिके टिकट का नंबर लौटाता है।
टिकट का नंबर इस बिक्री के बाद की गिनती है: जब गिनती 41 थी, तो यह बिक्री टिकट 42 है।
यह फंक्शन यह आर्गुमेंट लेता है:
tickets_sold: बिक चुके टिकटों की साझा गिनती का मेमोरी एड्रेस, एक 64-बिट साइन्ड पूर्णांक।tickets_sold = 0
// => 1, and tickets_sold is now 1
tickets_sold = 41
// => 42, and tickets_sold is now 42
यह फंक्शन टिकट का नंबर एक 64-बिट साइन्ड पूर्णांक के रूप में लौटाता है। टेस्ट एक ही समय में कई खिड़कियों से टिकट बिकवाते हैं: लौटाया गया हर नंबर अलग होना चाहिए, और अंत में गिनती बिक्री की संख्या के बराबर होनी चाहिए।
प्रीमियर में सीटों की संख्या तय है, और हर खिड़की उसी के टिकट बेच रही है। बिक्री तभी हो सकती है जब कोई सीट बची हो, और शो में कभी ज़रूरत से ज़्यादा टिकट नहीं बिकने चाहिए, चाहे हर खिड़की एक ही पल में अंतिम सीट के लिए हाथ बढ़ा रही हो। गिनती जाँचना और फिर उसे बढ़ाना, अगर ये दो अलग-अलग चरण रहें, तो बीच में एक मौका खुला रह जाता है; इसलिए जाँच और अपडेट या तो साथ सफल हों या साथ फेल।
claim_seat फंक्शन बनाइए, जो सीट बची होने पर एटॉमिक तरीके से अगली सीट ले लेता है।
सीट मिलने पर यह फंक्शन उस सीट का नंबर लौटाता है, जो इसके बाद भरी हुई सीटों की गिनती होती है।
शो हाउसफुल हो जाने पर यह 0 लौटाता है और गिनती को वैसा ही छोड़ देता है।
यह फंक्शन इस क्रम में ये आर्गुमेंट लेता है:
seats_taken: भरी जा चुकी सीटों की गिनती का मेमोरी एड्रेस, एक 64-बिट साइन्ड पूर्णांक, जो कभी capacity से ऊपर नहीं होता।capacity: शो में सीटों की संख्या, एक 64-बिट साइन्ड पूर्णांक, जो हमेशा 0 से बड़ा होता है।seats_taken = 0, capacity = 150
// => 1, and seats_taken is now 1
seats_taken = 82, capacity = 150
// => 83, and seats_taken is now 83
seats_taken = 149, capacity = 150
// => 150, and seats_taken is now 150
seats_taken = 150, capacity = 150
// => 0, and seats_taken stays 150
यह फंक्शन सीट का नंबर लौटाता है, या शो हाउसफुल हो जाने पर 0, एक 64-बिट साइन्ड पूर्णांक के रूप में।
प्रोजेक्शन बूथ में एक ही व्यक्ति बैठ सकता है।
मेमोरी का एक शब्द बताता है कि बूथ भरा है या खाली: 0 का मतलब खाली, 1 का मतलब भरा हुआ।
सारी रात कर्मचारी आते रहते हैं, और हर किसी को अंदर कोई काम करना होता है: लॉगबुक में हस्ताक्षर करना, प्रोजेक्टर में रील डालना, या रील को उसकी जगह रखना।
काम कोई भी हो, बूथ का नियम एक ही है, और उसे लागू करना आपका काम है: एक समय में अंदर सिर्फ़ एक व्यक्ति, उसके काम की शुरुआत से लेकर उसके पूरा होने तक।
visit_booth फंक्शन बनाइए, जो:
जब तक बूथ किसी और के पास हो, इसे pause के साथ शालीनता से इंतज़ार करना चाहिए।
यह फंक्शन इस क्रम में ये आर्गुमेंट लेता है:
booth: बूथ भरा है या खाली, यह बताने वाले शब्द का मेमोरी एड्रेस, एक 64-बिट साइन्ड पूर्णांक जो या तो 0 होता है या 1।task: आने वाले का काम, एक ऐसा फंक्शन जो कोई पैरामीटर नहीं लेता और जिसकी कोई रिटर्न वैल्यू नहीं होती।ध्यान रखिए कि टेस्ट ऐसे काम चलाते हैं जो बूथ को और एक साझा लॉगबुक को भी देखते हैं। इसका मतलब है कि पूरे काम के दौरान बूथ पर किसी एक का होना ज़रूरी है, हर मुलाक़ात अपना काम ठीक एक बार चलानी चाहिए, और एक साथ कई लोग आने पर लॉगबुक बिल्कुल सही निकलनी चाहिए।
आने वाले का काम एक साधारण फंक्शन है जो कॉलिंग कन्वेंशन का पालन करता है:
इनमें से किसी भी नियम का उल्लंघन फेल हुए चेक की जगह क्रैश के रूप में सामने आ सकता है।
Exercism पर साइन अप कीजिए और x86-64 Assembly को 22 कॉन्सेप्ट130 अभ्यास तथा असली इंसानों से मिलने वाली मेंटरिंग के साथ सीखिए और उसमें महारत हासिल कीजिए, वह भी बिल्कुल मुफ्त।