Треки
/
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-бітного операнда. Операнд памʼяті завжди має розмір операції: 8-бітний для pinsrb/pextrb, 16-бітний для 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, просто переміщують необроблені байти. Це означає, що їх також можна використовувати для значень із плаваючою комою, збережених у памʼяті або в регістрі загального призначення.

Вказівки

Ми пишемо внутрішні цикли програмного конвеєра зображень, того етапу, який готує текстури й поєднує шари, перш ніж вони потраплять на екран. Конвеєр працює з пікселями блок за блоком, застосовуючи ту саму операцію до всього блоку.

Піксель складається з чотирьох 1-байтових каналів: червоного, зеленого, синього й альфа, саме в такому порядку (RGBA). Блок містить 4 пікселі, тобто 16 байтів загалом.

На нас чекає пʼять завдань.

Операнди ми отримуємо через адреси памʼяті, а відповідь записуємо через адресу результату. Усі адреси памʼяті в цій вправі вирівняні на 16 байтів.

Note

Обчислення в цій вправі слід виконувати за допомогою SIMD-інструкцій, а не скалярних операцій.

1. Перетворити зображення на порядок відображення

Текстури зберігаються у форматі RGBA, але буфер кадру, у який малює цей конвеєр, приймає кожен піксель у порядку BGRA: червоний і синій канали міняються місцями, а зелений і альфа залишаються на місці. Зображення надходить як послідовність блоків, і кожен блок перетворюється однаково.

Реалізуйте функцію to_display_order, яка перетворює ціле зображення з RGBA на BGRA, блок за блоком. Керуючу маску для перевпорядкування каналів визначте як запаковану константу в памʼяті та повторно використовуйте її для кожного блоку.

Ця функція приймає такі аргументи, у цьому порядку:

  • result: адреса памʼяті буфера, куди записуються перетворені блоки, 16 байтів на блок.
  • pixels: адреса памʼяті вихідних блоків, 4 пікселі на блок, кожен піксель займає 4 байти в порядку 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: адреса памʼяті буфера, куди записуються заповнені блоки, 16 байтів на блок.
  • color: адреса памʼяті одного пікселя, 4 байти в порядку 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, яка переплітає два рядки по 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 робочих значень в один рядок із 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: адреса памʼяті буфера, куди записуються 4 координати x, 4 32-бітові числа з плаваючою комою.
  • ys: адреса памʼяті буфера, куди записуються 4 координати 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 вправ та справжнє наставництво від людей, і все це безкоштовно.