سوئیزل کانال

سوئیزل کانال

تمرین یادگیری

مقدمه

SIMD: عملیات بین‌خطی

هر عملیات SIMD تا اینجا خط به خط عمل کرده است. مقدار موجود در خط i از نتیجه، بر اساس خط i از ورودی‌ها محاسبه می‌شود.

با این حال، موقعیت‌های زیادی وجود دارد که جابه‌جایی مقادیر بین خط‌ها لازم یا مطلوب است. برای مثال، ممکن است خط‌ها برای محاسبه‌ای که باید انجام دهیم، نامرتب باشند.

دستورهای SIMD زیادی وجود دارند که داده را به روش‌های مختلف بین خط‌ها جابه‌جا می‌کنند.

درهم‌ریزی

یک درهم‌ریزی ترتیب بایت‌های یا خط‌های یک ثبات را تغییر می‌دهد و هر خط مقصد را از یک خط مبدأ که ممکن است هر جایی باشد برمی‌دارد. آن‌ها می‌توانند یک خط را به مقصدهای مختلف انتخاب کنند، که باعث می‌شود بتوانند مقادیر را نیز پخش کنند.

دستورهای درهم‌ریزی بسته به اندازه و دامنه‌ی اجرایشان رفتار متفاوتی دارند.

انتخاب خط‌ها با یک مقدار فوری

دستور pshufd چهار خط ۳۲ بیتی مبدأ خود را در مقصد بازآرایی می‌کند.

انتخاب یک مقدار فوری ۸ بیتی است که به صورت چهار فیلد ۲ بیتی خوانده می‌شود، هر فیلد برای یک خط مقصد. هر فیلد انتخاب می‌کند که کدام یک از چهار خط مبدأ در آن خط مقصد کپی شود:

فیلد شماره‌ی خط
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 از همان رمزگذاری فیلد ۲ بیتی درهم‌ریزی‌های عدد صحیح قبلی استفاده می‌کند. اما چون در یک عملوند ۱۲۸ بیتی فقط ۲ خط ۶۴ بیتی وجود دارد، shufpd فقط از رمزگذاری فیلد ۱ بیتی استفاده می‌کند.

این دستورها با همتایان عدد صحیح خود تفاوت دارند از این جهت که خط‌ها را از دو عملوند به جای یکی برمی‌دارند:

  • نیمه‌ی پایین نتیجه از عملوند مقصد گرفته می‌شود.
  • نیمه‌ی بالای آن از عملوند مبدأ گرفته می‌شود.
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 یا یک عملوند حافظه‌ی ۱۶ بایتی است.

برای هر یک از ۱۶ خط مقصد، چهار بیت پایین بردار کنترل یک شماره‌ی بایت مبدأ از ۰ تا ۱۵ می‌دهند. اگر خط i بردار کنترل شماره‌ی بایت مبدأ j را داشته باشد، آنگاه خط j-th مقصد به خط i-th منتقل می‌شود.

این تفاوت دوم بین دو دستور را برجسته می‌کند. در حالی که 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 مقصد صفر می‌شود. این باعث می‌شود pshufb هم یک جایگشت دلخواه بایت و هم یک پاک‌سازی گزینشی باشد، همه در یک دستور.

چون pshufb در سطح دانه‌بندی بایت عمل می‌کند، می‌توان از آن برای درهم‌ریزی خط‌هایی با اندازه‌های مختلف نیز استفاده کرد، با گروه‌بندی خط‌های همسایه. برای مثال، می‌توان از آن برای درهم‌ریزی dwordها استفاده کرد:

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 (پسوند اندازه برای ۱۶ بایت 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)

این دستورها اشباع‌کننده هستند، یعنی مقادیر خروجی به محدوده‌ی باریک‌تر محدود می‌شوند. آن‌ها از dword به کلمه و از کلمه به بایت عمل می‌کنند. گونه‌ای از qword به dword وجود ندارد.

توجه کنید که ورودی همیشه علامت‌دار تفسیر می‌شود. نوع، چه علامت‌دار و چه بدون علامت، برای خروجی است. این نوع محدوده‌ای را نشان می‌دهد که باید محدود شود. برای مثال، 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).

در هر دو مورد، ثبات همه‌کاره معمولاً ۳۲ بیتی است. فقط pinsrq و pextrq به یک عملوند ۶۴ بیتی نیاز دارند. یک عملوند حافظه همیشه به اندازه‌ی عملیات است: ۸ بیتی برای pinsrb/pextrb، ۱۶ بیتی برای pinsrw/pextrw، و به همین ترتیب.

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). هر بلوک ۴ پیکسل است، یعنی در مجموع ۱۶ بایت.

شما پنج وظیفه دارید.

عملوندها را از طریق آدرس‌های حافظه دریافت می‌کنید و پاسخ خود را از طریق یک آدرس نتیجه می‌نویسید. همه‌ی آدرس‌های حافظه در این تمرین روی مرزهای ۱۶ بایتی هم‌تراز هستند.

Note

محاسبات این تمرین باید با دستورهای SIMD انجام شوند، نه با عملیات اسکالر.

1. تبدیل یک تصویر به ترتیب نمایش

بافت‌ها به‌صورت 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}

این تابع مقدار بازگشتی ندارد.

2. پر کردن یک ناحیه با یک رنگ

برای پاک کردن یک ناحیه یا رنگ‌آمیزی یک گستره‌ی تخت، خط لوله یک رنگ واحد را روی هر پیکسل آن ناحیه می‌نویسد.

تابع 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}

این تابع مقدار بازگشتی ندارد.

3. درهم‌بافتن دو خط اسکن

دو لایه‌ی تک‌کاناله باید در یک بافر ادغام شوند، به‌طوری که نمونه‌هایشان درهم‌بافته شوند. نتیجه به‌تناوب یک نمونه از لایه‌ی اول و سپس یک نمونه از لایه‌ی دوم را می‌آورد.

تابع 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}

این تابع مقدار بازگشتی ندارد.

4. بسته‌بندی یک ردیف روشن‌شده

یک مرحله‌ی روشنایی هر نمونه را با دقت کاری ۱۶ بیتی مقیاس می‌کند، به‌طوری که یک نمونه‌ی بیش‌ازحد روشن می‌تواند از 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}

این تابع مقدار بازگشتی ندارد.

5. تقسیم مختصات به x و y

داده‌های بافت و رأس اغلب درهم‌آمیخته می‌رسند و 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}

این تابع مقدار بازگشتی ندارد.

ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
x86-64 Assembly Exercism

آماده‌اید سوئیزل کانال را شروع کنید؟

در Exercism ثبت‌نام کنید تا x86-64 Assembly را همراه با 22 مفهوم130 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.