এখন পর্যন্ত প্রতিটি 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-বিট অপারেন্ডে মাত্র ২টি 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-বাইটের মেমোরি অপারেন্ড।
১৬টি ডেস্টিনেশন লেনের প্রতিটির জন্য কন্ট্রোল ভেক্টরের নিচের চারটি বিট একটি সোর্স বাইট ইনডেক্স দেয়, 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}
আনপ্যাক ইনস্ট্রাকশনগুলো দুইটি অপারেন্ড থেকে লেন নিয়ে সেগুলো একসাথে বুনে দেয়, পালাক্রমে দুইটির মধ্যে ঘুরে ঘুরে। এগুলোর ইন্টিজার ও ফ্লোট ভ্যারিয়েন্ট আছে এবং এরা মূলত একই সাধারণ সিনট্যাক্স গঠন অনুসরণ করে, তবে দুইটি পার্থক্য নিয়ে:
l বা h সাফিক্স থাকে, যা বোঝায় এটি প্রতিটি অপারেন্ডের নিচের অর্ধেক (l) নাকি উপরের অর্ধেক (h)-এ কাজ করে।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]}
এই ইনস্ট্রাকশনগুলোর কোনো ফ্লোটিং-পয়েন্ট সমতুল্য নেই।
এখন পর্যন্ত আমরা সবসময় movq/movd ব্যবহার করে SIMD রেজিস্টার আর জেনারেল-পারপাস রেজিস্টারের মধ্যে ডেটা সরিয়েছি।
এই ইনস্ট্রাকশনগুলো 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-এর মতোই শুধু কাঁচা বাইট সরায়।
এর মানে এগুলো মেমোরিতে বা জেনারেল-পারপাস রেজিস্টারে রাখা ফ্লোটিং-পয়েন্ট মানের জন্যও ব্যবহার করা যায়।
আপনি একটি সফটওয়্যার ইমেজ পাইপলাইনের ভেতরের লুপগুলো লেখেন, যে ধাপটি টেক্সচার প্রস্তুত করে আর স্তরগুলো স্ক্রিনে পৌঁছানোর আগে সেগুলো কম্পোজিট করে। পাইপলাইনটি একবারে একটি ব্লক ধরে পিক্সেল নিয়ে কাজ করে, পুরো ব্লকজুড়ে একই অপারেশন প্রয়োগ করে।
একটি পিক্সেল চারটি ১-বাইট চ্যানেল নিয়ে গঠিত: লাল, সবুজ, নীল আর আলফা, ঠিক এই ক্রমে (RGBA)।
একটি ব্লকে ৪টি পিক্সেল, অর্থাৎ মোট ১৬ বাইট।
আপনার সামনে পাঁচটি কাজ আছে।
আপনি অপারেন্ডগুলো পান মেমোরি অ্যাড্রেসের মাধ্যমে, আর আপনার উত্তর লেখেন একটি ফলাফল অ্যাড্রেসে। এই অনুশীলনীর সব মেমোরি অ্যাড্রেস ১৬-বাইট অ্যালাইনড।
এই অনুশীলনীর গণনাগুলো স্কেলার অপারেশনে নয়, বরং SIMD ইনস্ট্রাকশন ব্যবহার করে করা উচিত।
টেক্সচার RGBA হিসেবে সংরক্ষিত থাকে, কিন্তু এই পাইপলাইন যে ফ্রেমবাফারে আঁকে সেটি প্রতিটি পিক্সেল BGRA ক্রমে আশা করে: লাল ও নীল চ্যানেল অদলবদল করা, সবুজ ও আলফা চ্যানেল আগের জায়গায় রাখা।
একটি ছবি ব্লকের একটি ক্রম হিসেবে আসে, আর প্রতিটি ব্লক একইভাবে রূপান্তরিত হয়।
to_display_order ফাংশনটি ইমপ্লিমেন্ট করুন, যেটি একটি সম্পূর্ণ ছবিকে RGBA থেকে BGRA-তে রূপান্তর করে, একবারে একটি ব্লক ধরে।
চ্যানেল পুনর্বিন্যাসের কন্ট্রোল মাস্কটি মেমোরিতে একটি প্যাকড কনস্ট্যান্ট হিসেবে ডিফাইন করা উচিত, এবং প্রতিটি ব্লকে সেটি পুনরায় ব্যবহার করুন।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
result: যে বাফারে রূপান্তরিত ব্লকগুলো লেখা হয় তার মেমোরি অ্যাড্রেস, প্রতি ব্লকে ১৬ বাইট।pixels: সোর্স ব্লকগুলোর মেমোরি অ্যাড্রেস, প্রতি ব্লকে ৪ পিক্সেল, প্রতিটি পিক্সেল RGBA ক্রমে ৪ বাইট।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}
এই ফাংশনটির কোনো রিটার্ন ভ্যালু নেই।
কোনো অঞ্চল পরিষ্কার করতে বা একটি সমান স্প্যান আঁকতে, পাইপলাইন অঞ্চলটির প্রতিটি পিক্সেলে একটিমাত্র রঙ লেখে।
fill_region ফাংশনটি ইমপ্লিমেন্ট করুন, যেটি block_count ব্লকের একটি অঞ্চলকে একটি রঙের কপি দিয়ে ভরিয়ে দেয়।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
result: যে বাফারে ভরা ব্লকগুলো লেখা হয় তার মেমোরি অ্যাড্রেস, প্রতি ব্লকে ১৬ বাইট।color: একটি পিক্সেলের মেমোরি অ্যাড্রেস, RGBA ক্রমে ৪ বাইট।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}
এই ফাংশনটির কোনো রিটার্ন ভ্যালু নেই।
দুটি একক-চ্যানেল লেয়ারকে একটি বাফারে একত্র করা দরকার, যেখানে তাদের স্যাম্পলগুলো ইন্টারলিভ করা থাকবে। ফলাফলে প্রথম লেয়ার থেকে একটি স্যাম্পল, তারপর দ্বিতীয় লেয়ার থেকে একটি, এভাবে পালা করে আসে।
weave_scanlines ফাংশনটি ইমপ্লিমেন্ট করুন, যেটি ১৬টি করে স্যাম্পলের দুটি সারিকে ইন্টারলিভ করে ৩২টি স্যাম্পলের একটি সারি বানায়।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
result: যে বাফারে ৩২টি ইন্টারলিভ করা স্যাম্পল লেখা হয় তার মেমোরি অ্যাড্রেস।first: প্রথম সারির মেমোরি অ্যাড্রেস, ১৬টি স্যাম্পল, প্রতিটি ৮-বিট মান।second: দ্বিতীয় সারির মেমোরি অ্যাড্রেস, ১৬টি স্যাম্পল, প্রতিটি ৮-বিট মান।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}
এই ফাংশনটির কোনো রিটার্ন ভ্যালু নেই।
একটি ব্রাইটনেস পাস প্রতিটি স্যাম্পলকে ১৬-বিট ওয়ার্কিং প্রেসিশনে স্কেল করে, যাতে অতিরিক্ত উজ্জ্বল একটি স্যাম্পল 255 ছাড়িয়ে যেতে পারে আর একটি পার্থক্য 0-এর নিচে নামতে পারে।
শেষ ধাপে ওই ওয়ার্কিং মানগুলোকে ডিসপ্লের জন্য আবার ৮-বিট স্যাম্পলে সংকুচিত করা হয়, 0-এর নিচের যেকোনো কিছুকে উপরে তুলে 0 করা হয় আর 255-এর উপরের যেকোনো কিছুকে নিচে নামিয়ে 255 করা হয়।
pack_samples ফাংশনটি ইমপ্লিমেন্ট করুন, যেটি ৮টি করে ওয়ার্কিং মানের দুটি গ্রুপকে ক্রমানুসারে ১৬টি স্যাম্পলের একটি সারিতে সংকুচিত করে।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
result: যে বাফারে ১৬টি ক্ল্যাম্প করা স্যাম্পল লেখা হয় তার মেমোরি অ্যাড্রেস, প্রতিটি ৮-বিট মান।first: প্রথম ৮টি ওয়ার্কিং মানের মেমোরি অ্যাড্রেস, প্রতিটি ১৬-বিট সাইনড ইন্টিজার।second: পরের ৮টি ওয়ার্কিং মানের মেমোরি অ্যাড্রেস, প্রতিটি ১৬-বিট সাইনড ইন্টিজার।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}
এই ফাংশনটির কোনো রিটার্ন ভ্যালু নেই।
টেক্সচার ও ভার্টেক্স ডেটা প্রায়ই ইন্টারলিভ অবস্থায় আসে, প্রতিটি পয়েন্টের x ও y একসাথে প্যাক করা থাকে।
তবে দক্ষ প্রসেসিংয়ের জন্য এগুলো আলাদা থাকা প্রায়ই দরকার: সব x মান একটি ভেক্টরে, সব y মান আরেকটিতে।
split_coordinates ফাংশনটি ইমপ্লিমেন্ট করুন, যেটি ইন্টারলিভ করা চারটি (x, y) পয়েন্টকে x স্থানাঙ্কের একটি ভেক্টর ও y স্থানাঙ্কের একটি ভেক্টরে আলাদা করে।
এই ফাংশনটি আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
xs: যে বাফারে ৪টি x স্থানাঙ্ক লেখা হয় তার মেমোরি অ্যাড্রেস, ৪টি ৩২-বিট ফ্লোটিং-পয়েন্ট সংখ্যা।ys: যে বাফারে ৪টি y স্থানাঙ্ক লেখা হয় তার মেমোরি অ্যাড্রেস, ৪টি ৩২-বিট ফ্লোটিং-পয়েন্ট সংখ্যা।first: প্রথম দুটি পয়েন্টের মেমোরি অ্যাড্রেস, ৪টি ৩২-বিট ফ্লোটিং-পয়েন্ট সংখ্যা, {x0, y0, x1, y1} আকারে।second: পরের দুটি পয়েন্টের মেমোরি অ্যাড্রেস, ৪টি ৩২-বিট ফ্লোটিং-পয়েন্ট সংখ্যা, {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}
এই ফাংশনটির কোনো রিটার্ন ভ্যালু নেই।
Exercism-এ সাইন আপ করুন, x86-64 Assembly ট্র্যাকের 22টি কনসেপ্ট130টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।