ट्रैक
/
x86-64 Assembly
x86-64 Assembly
/
अभ्यास
/
चैनल स्विज़ल
चैनल स्विज़ल

चैनल स्विज़ल

सीखने का अभ्यास

परिचय

SIMD: लेनों के आर-पार के ऑपरेशन

अब तक हर SIMD ऑपरेशन एक-एक लेन पर काम करता आया है। नतीजे की लेन i की वैल्यू इनपुट की उसी लेन i पर गणना करके निकाली जाती है।

लेकिन बहुत सी ऐसी स्थितियाँ होती हैं जहाँ लेनों के बीच वैल्यू ले जाना ज़रूरी होता है या चाहा जाता है। जैसे, हमें जो गणना करनी है उसके लिए लेनें क्रम में नहीं होंगी।

बहुत से SIMD निर्देश ऐसे हैं जो डेटा को लेनों के आर-पार अलग-अलग तरीकों से ले जाते हैं।

शफलिंग

एक शफल रजिस्टर के बाइट्स या लेनों का क्रम बदल देता है, और हर गंतव्य लेन को किसी भी जगह की स्रोत लेन से खींचता है। ये एक ही लेन को अलग-अलग गंतव्यों के लिए चुन सकते हैं, इसलिए ये वैल्यू फैलाने का काम भी कर सकते हैं।

शफलिंग निर्देश अपने आकार और एक्ज़ीक्यूशन डोमेन के अनुसार अलग-अलग व्यवहार करते हैं।

इमीडिएट से लेनें चुनना

pshufd निर्देश अपने स्रोत की चारों 32-बिट लेनों को गंतव्य में नए क्रम में रख देता है।

यह चयन एक 8-बिट इमीडिएट है, जिसे चार 2-बिट फील्ड के रूप में पढ़ा जाता है, हर गंतव्य लेन के लिए एक। हर फील्ड तय करता है कि चार स्रोत लेनों में से कौन-सी उस गंतव्य लेन में कॉपी की जाएगी:

फील्ड लेन इंडेक्स
00 0
01 1
10 2
11 3

इमीडिएट में फील्ड की स्थिति, जिसे दाएँ से बाएँ पढ़ा जाता है, बताती है कि चुनी गई लेन कहाँ रखी जाएगी। एक स्रोत लेन को एक से ज़्यादा बार चुना जा सकता है, और इसी तरह एक ही लेन पूरे रजिस्टर में फैलाई जाती है:

; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
फ्लोटिंग-पॉइंट लेनों को शफल करना

फ्लोटिंग-पॉइंट के लिए दो शफल निर्देश हैं, जो एक ही सामान्य सिंटैक्स का पालन करते हैं: shuf + p + आकार का सफिक्स (या तो s या d)।

ये भी लेन चुनने के लिए एक इमीडिएट का इस्तेमाल करते हैं। shufps पिछले इंटीजर शफलों की तरह ही 2-बिट फील्ड एन्कोडिंग का इस्तेमाल करता है। लेकिन 128-बिट ऑपरेंड में सिर्फ 2 ही 64-बिट लेनें होती हैं, इसलिए shufpd सिर्फ 1-बिट फील्ड एन्कोडिंग का इस्तेमाल करता है।

ये निर्देश अपने इंटीजर रूपों से इस मायने में अलग हैं कि ये लेनें एक नहीं, दो ऑपरेंड से लेते हैं:

  • नतीजे का निचला आधा हिस्सा गंतव्य ऑपरेंड से लिया जाता है।
  • ऊपरी आधा हिस्सा स्रोत ऑपरेंड से लिया जाता है।
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1         ; xmm4 = {xmm4[1], xmm5[0]}

चूँकि दोनों ऑपरेंड नतीजे में योगदान देते हैं, इनका इस्तेमाल एक ही चरण में दो वेक्टर इंटरलीव करने के लिए किया जा सकता है। अगर स्रोत और गंतव्य एक ही रजिस्टर हों, तो हर लेन उसी से खींची जाती है।

बाइट शफल

pshufb सबसे सामान्य शफल है। हालाँकि pshufb और pshufd के नाम बहुत मिलते-जुलते हैं और सिर्फ आकार के सफिक्स में फर्क है, ये दोनों बिलकुल अलग काम करते हैं।

पहला फर्क यह है कि जहाँ pshufd लेन की जगहें चुनने के लिए इमीडिएट का इस्तेमाल करता है, वहीं pshufb स्रोत ऑपरेंड में एक कंट्रोल वेक्टर का इस्तेमाल करता है। यह कंट्रोल वेक्टर एक xmm रजिस्टर या 16-बाइट मेमोरी ऑपरेंड होता है।

16 गंतव्य लेनों में से हर एक के लिए, कंट्रोल वेक्टर के निचले चार बिट एक स्रोत बाइट इंडेक्स देते हैं, 0 से 15 तक। अगर कंट्रोल वेक्टर की लेन i में स्रोत बाइट इंडेक्स j है, तो गंतव्य की j-th-वीं लेन i-वीं लेन पर ले जाई जाएगी।

यह इन दोनों निर्देशों के बीच दूसरा फर्क दिखाता है। जहाँ pshufd शफल होने वाली लेनें किसी अलग स्रोत ऑपरेंड से लेता है, वहीं pshufb शफलिंग गंतव्य में ही उसी जगह कर देता है।

section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

section .text
fn:
  pshufb xmm0, [rel reverse]
  ; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
  ; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
  ; ...
  ; in the end, the bytes in xmm0 are reversed

pshufb इससे भी ज़्यादा लचीला है: यह किसी भी लेन को साफ़ कर सकता है। अगर कंट्रोल वेक्टर की लेन i में सबसे ऊपर वाला बिट सेट हो, तो गंतव्य की लेन i-th शून्य कर दी जाती है। इस तरह pshufb एक ही निर्देश में बाइट्स का मनचाहा क्रम बदलने और चुन-चुनकर साफ़ करने, दोनों का काम करता है।

चूँकि pshufb बाइट के स्तर पर काम करता है, इसे अलग-अलग आकार की लेनों को शफल करने के लिए भी इस्तेमाल किया जा सकता है, बस पड़ोसी लेनों को एक साथ समूहित करके। जैसे, इससे डीवर्ड शफल किए जा सकते हैं:

section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes

section .text
fn:
  movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
  pshufb xmm0, [rel reverse_dwords]
  ; xmm0 = {49, 37, 25, 13}

लेनों को इंटरलीव करना

अनपैक निर्देश दो ऑपरेंड से लेनें लेते हैं और उन्हें बारी-बारी से एक साथ बुन देते हैं। इनके इंटीजर और फ्लोट रूप होते हैं, और ये लगभग एक ही सामान्य सिंटैक्स ढाँचे का पालन करते हैं, बस दो फर्क के साथ:

  1. एक l या h सफिक्स होता है, जो बताता है कि यह हर ऑपरेंड के निचले आधे (l) पर काम करता है या ऊपरी आधे (h) पर।
  2. इंटीजर रूप दो आकार सफिक्स लेते हैं, जिनमें दूसरा पहले से दोगुना होता है। जैसे, bw या qdq (16 बाइट्स का आकार सफिक्स dq है, जैसे movdqu में)।
punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}

unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}

संतृप्ति के साथ छोटा करना

पैक निर्देश दूसरी दिशा में काम करते हैं, यानी दो ऑपरेंड की लेनों को मिलाकर गंतव्य की आधी चौड़ाई वाली लेनों में डाल देते हैं।

इन निर्देशों में p प्रीफिक्स नहीं होता। इसके अलावा, इनका सिंटैक्स उन चीज़ों को जोड़ता है जो हम पहले ही देख चुके हैं:

  • जो काम किया जा रहा है, यानी pack।
  • एक s या u, जो बताता है कि आउटपुट वैल्यू साइन्ड हैं या अनसाइन्ड।
  • एक s संतृप्त करने के लिए, जैसा पिछले कॉन्सेप्ट में देखे गए संतृप्त अंकगणित में था।
  • दो आकार सफिक्स, जिनमें दूसरा पहले की चौड़ाई का आधा बताता है (unpck के उलट व्यवहार)।

चूँकि पैक करने का काम चीज़ों को छोटा करता है, इसे l या h सफिक्स की ज़रूरत नहीं होती:

packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)

ये निर्देश संतृप्त करने वाले हैं, यानी आउटपुट वैल्यू को छोटी रेंज तक सीमित कर दिया जाता है। ये डीवर्ड से वर्ड तक और वर्ड से बाइट तक काम करते हैं। क्यूवर्ड से डीवर्ड वाला कोई रूप नहीं है।

ध्यान दीजिए कि इनपुट को हमेशा साइन्ड माना जाता है। साइन्ड या अनसाइन्ड जो टाइप है, वह आउटपुट के लिए होता है। यह बताता है कि किस रेंज तक सीमित करना है। जैसे, packsswb साइन्ड बाइट की रेंज तक सीमित करता है, यानी [-128, 127]।

नतीजे की निचली लेनें गंतव्य ऑपरेंड से आती हैं और ऊपरी लेनें स्रोत से:

packusdw xmm0, xmm1   ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}

इन निर्देशों का फ्लोटिंग-पॉइंट में कोई समकक्ष नहीं है।

अलग-अलग लेनें ले जाना

अब तक हमने डेटा को SIMD रजिस्टर और जनरल-पर्पस रजिस्टर के बीच हमेशा movq/movd की मदद से ले जाया है। ये निर्देश SIMD रजिस्टर की सिर्फ निचली लेन में लिख सकते हैं या उससे पढ़ सकते हैं, और लिखते समय बाकी सारी लेनें साफ़ कर देते हैं।

ऐसे निर्देश भी हैं जो यही काम किसी भी लेन के लिए करते हैं, सिर्फ पहली के लिए नहीं, और बाकी लेनों को छेड़ते नहीं:

  • इंसर्ट निर्देश जनरल-पर्पस रजिस्टर या मेमोरी से एक एलिमेंट को इमीडिएट से चुनी गई लेन में लिखते हैं।
  • एक्सट्रैक्ट निर्देश किसी एक लेन से पढ़कर जनरल-पर्पस रजिस्टर में डालते हैं।

दोनों इंटीजर सिंटैक्स का पालन करते हैं: p + insr/extr + आकार सफिक्स (b, w, d, या q)।

दोनों ही मामलों में जनरल-पर्पस रजिस्टर आमतौर पर 32-बिट चौड़ा होता है। सिर्फ pinsrq और pextrq को 64-बिट ऑपरेंड चाहिए। मेमोरी ऑपरेंड हमेशा उसी काम के आकार का होता है: pinsrb/pextrb के लिए 8-बिट, pinsrw/pextrw के लिए 16-बिट, वगैरह।

pinsrb xmm0, eax, 5        ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx

ध्यान दीजिए कि ये निर्देश, movd और movq की तरह ही, बस कच्चे बाइट्स को ले जाते हैं। इसका मतलब है कि इन्हें मेमोरी या जनरल-पर्पस रजिस्टर में रखी फ्लोटिंग-पॉइंट वैल्यू के लिए भी इस्तेमाल किया जा सकता है।

निर्देश

आप एक सॉफ्टवेयर इमेज पाइपलाइन के अंदरूनी लूप लिखते हैं, यानी उस चरण के, जो टेक्सचर तैयार करता है और स्क्रीन तक पहुँचने से पहले परतों को जोड़ता है। यह पाइपलाइन एक बार में एक ब्लॉक पर काम करती है और पूरे ब्लॉक पर वही ऑपरेशन लागू करती है।

एक पिक्सेल चार 1-बाइट चैनलों से बना होता है: लाल, हरा, नीला और अल्फा, इसी क्रम में (RGBA)। एक ब्लॉक में 4 पिक्सेल होते हैं, यानी कुल 16 बाइट।

इस अभ्यास में आपके पाँच कार्य हैं।

आपको ऑपरेंड मेमोरी एड्रेस के ज़रिए मिलते हैं, और आप अपना उत्तर रिज़ल्ट एड्रेस के ज़रिए लिखते हैं। इस अभ्यास में सभी मेमोरी एड्रेस 16-बाइट अलाइन्ड होते हैं।

Note

इस अभ्यास की गणनाएँ स्केलर ऑपरेशन से नहीं, बल्कि SIMD निर्देशों का उपयोग करके की जानी चाहिए।

1. इमेज को प्रदर्शन क्रम में बदलिए

टेक्सचर RGBA के रूप में संग्रहीत होते हैं, लेकिन जिस फ्रेमबफर में यह पाइपलाइन चित्र बनाती है, वह हर पिक्सेल को BGRA क्रम में चाहता है: लाल और नीले चैनल आपस में बदले हुए, और हरे तथा अल्फा चैनल अपनी जगह पर। इमेज ब्लॉकों के क्रम में आती है, और हर ब्लॉक उसी तरह बदला जाता है।

to_display_order फंक्शन बनाइए, जो पूरी इमेज को एक बार में एक ब्लॉक करके RGBA से BGRA में बदलता है। आपको चैनलों का क्रम बदलने वाले कंट्रोल मास्क को मेमोरी में एक पैक्ड कॉन्स्टेंट के रूप में परिभाषित करना चाहिए, और हर ब्लॉक के लिए उसी का दोबारा उपयोग करना चाहिए।

यह फंक्शन इस क्रम में ये आर्गुमेंट लेता है:

  • result: उस बफर का मेमोरी एड्रेस जहाँ बदले हुए ब्लॉक लिखे जाते हैं, हर ब्लॉक 16 बाइट।
  • pixels: स्रोत ब्लॉकों का मेमोरी एड्रेस, हर ब्लॉक में 4 पिक्सेल, और हर पिक्सेल RGBA क्रम में 4 बाइट।
  • block_count: ब्लॉकों की संख्या, जो हमेशा 0 से अधिक होती है।
pixels      = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
               1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result      = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
               3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}

यह फंक्शन कोई वैल्यू नहीं लौटाता।

2. किसी क्षेत्र को एक रंग से भरिए

किसी क्षेत्र को खाली करने या उसे एक ही रंग से भरने के लिए, पाइपलाइन उस क्षेत्र के हर पिक्सेल पर वही रंग लिखती है।

fill_region फंक्शन बनाइए, जो block_count ब्लॉकों के एक क्षेत्र को एक ही रंग की प्रतियों से भरता है।

यह फंक्शन इस क्रम में ये आर्गुमेंट लेता है:

  • result: उस बफर का मेमोरी एड्रेस जहाँ भरे हुए ब्लॉक लिखे जाते हैं, हर ब्लॉक 16 बाइट।
  • color: एक पिक्सेल का मेमोरी एड्रेस, RGBA क्रम में 4 बाइट।
  • block_count: भरे जाने वाले ब्लॉकों की संख्या, जो हमेशा 0 से अधिक होती है।
color       = {18, 52, 86, 120}
block_count = 2
result      = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
               18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}

यह फंक्शन कोई वैल्यू नहीं लौटाता।

3. दो स्कैनलाइनों को बुनिए

दो सिंगल-चैनल परतों को एक बफर में इस तरह मिलाना है कि उनके सैंपल बारी-बारी आएँ। नतीजे में पहली परत का एक सैंपल, फिर दूसरी परत का एक सैंपल, इसी तरह बारी-बारी आता है।

weave_scanlines फंक्शन बनाइए, जो 16-16 सैंपलों की दो पंक्तियों को बारी-बारी मिलाकर 32 सैंपलों की एक पंक्ति बनाता है।

यह फंक्शन इस क्रम में ये आर्गुमेंट लेता है:

  • result: उस बफर का मेमोरी एड्रेस जहाँ बारी-बारी मिले 32 सैंपल लिखे जाते हैं।
  • first: पहली पंक्ति का मेमोरी एड्रेस, 16 सैंपल, और हर सैंपल एक 8-बिट वैल्यू।
  • second: दूसरी पंक्ति का मेमोरी एड्रेस, 16 सैंपल, और हर सैंपल एक 8-बिट वैल्यू।
first  = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
          8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}

यह फंक्शन कोई वैल्यू नहीं लौटाता।

4. चमकाई गई पंक्ति को पैक कीजिए

ब्राइटनेस पास हर सैंपल को 16-बिट वर्किंग प्रिसीज़न में स्केल करता है, जिससे कि ज़रूरत से ज़्यादा चमकीला सैंपल 255 से ऊपर जा सकता है और कोई अंतर 0 से नीचे गिर सकता है। अंतिम चरण उन वर्किंग वैल्यू को दिखाने के लिए वापस 8-बिट सैंपल में सिकोड़ देता है, जिसमें 0 से नीचे की हर वैल्यू को ऊपर उठाकर 0 कर दिया जाता है और 255 से ऊपर की हर वैल्यू को नीचे लाकर 255 कर दिया जाता है।

pack_samples फंक्शन बनाइए, जो 8-8 वर्किंग वैल्यू के दो समूहों को क्रम से सिकोड़कर 16 सैंपलों की एक पंक्ति बनाता है।

यह फंक्शन इस क्रम में ये आर्गुमेंट लेता है:

  • result: उस बफर का मेमोरी एड्रेस जहाँ सीमित किए गए 16 सैंपल लिखे जाते हैं, हर एक 8-बिट वैल्यू।
  • first: पहली 8 वर्किंग वैल्यू का मेमोरी एड्रेस, हर एक 16-बिट साइन्ड पूर्णांक।
  • second: अगली 8 वर्किंग वैल्यू का मेमोरी एड्रेस, हर एक 16-बिट साइन्ड पूर्णांक।
first  = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200,   255, 255, 0, 100, 50, 255, 7, 0}

यह फंक्शन कोई वैल्यू नहीं लौटाता।

5. निर्देशांकों को x और y में बाँटिए

टेक्सचर और वर्टेक्स डेटा अक्सर बारी-बारी मिला हुआ आता है, जिसमें हर बिंदु के x और y एक साथ पैक होते हैं। लेकिन कुशल प्रोसेसिंग के लिए अक्सर उन्हें अलग-अलग रखना ज़रूरी होता है: सारी x वैल्यू एक वेक्टर में, और सारी y वैल्यू दूसरे में।

split_coordinates फंक्शन बनाइए, जो बारी-बारी मिले चार (x, y) बिंदुओं को अलग करके x निर्देशांकों का एक वेक्टर और y निर्देशांकों का एक वेक्टर बनाता है।

यह फंक्शन इस क्रम में ये आर्गुमेंट लेता है:

  • xs: उस बफर का मेमोरी एड्रेस जहाँ चारों x निर्देशांक लिखे जाते हैं, 4 32-बिट दशमलव संख्याएँ।
  • ys: उस बफर का मेमोरी एड्रेस जहाँ चारों y निर्देशांक लिखे जाते हैं, 4 32-बिट दशमलव संख्याएँ।
  • first: पहले दो बिंदुओं का मेमोरी एड्रेस, 4 32-बिट दशमलव संख्याएँ, इस रूप में {x0, y0, x1, y1}।
  • second: अगले दो बिंदुओं का मेमोरी एड्रेस, 4 32-बिट दशमलव संख्याएँ, इस रूप में {x2, y2, x3, y3}।
first  = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs     = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys     = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}

यह फंक्शन कोई वैल्यू नहीं लौटाता।

GitHub के ज़रिए संपादित करें यह लिंक एक नई विंडो या टैब में खुलता है
x86-64 Assembly Exercism

चैनल स्विज़ल शुरू करने के लिए तैयार हैं?

Exercism पर साइन अप कीजिए और x86-64 Assembly को 22 कॉन्सेप्ट130 अभ्यास तथा असली इंसानों से मिलने वाली मेंटरिंग के साथ सीखिए और उसमें महारत हासिल कीजिए, वह भी बिल्कुल मुफ्त।