Треки
/
x86-64 Assembly
x86-64 Assembly
/
Вправи
/
Відомість оцінок
Відомість оцінок

Відомість оцінок

Навчальна вправа

Вступ

SIMD: маски та умови

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

Ось чому код SIMD типово не містить розгалужень.

Замість того щоб покладатися на прапорці, упаковані порівняння зазвичай створюють маску в операнді призначення. Для кожної доріжки порівняння заповнює цілу доріжку одиницями, коли умова правдива, і нулями, коли вона хибна. Якщо читати як ціле число зі знаком, правдива доріжка дорівнює -1, а хибна 0.

Потім цю маску можна поєднати з побітовими операціями, щоб відфільтрувати конкретні доріжки.

Упаковані порівняння

Скалярна інструкція cmp є загальною в тому сенсі, що її використовують, щоб одночасно встановити різні прапорці. Інша інструкція може потім спожити ці прапорці, щоб або розгалузитися, або виконати обчислення.

Однак, оскільки упаковане порівняння і перевіряє умову, і обчислює маску, воно не є загальним. Порівнянню потрібно передати точну умову, яку перевіряють.

Є два способи зробити це:

  • Порівняння цілих чисел отримують умову як суфікс: eq для рівності та gt для «більше ніж». Інші варіанти будують, поєднуючи результат одного з них.
  • Порівняння чисел з плаваючою комою отримують умову, закодовану в безпосередньому значенні. Різні значення цього безпосереднього значення відповідають різним умовам, які перевіряють.

Окрім використання конкретного умовного суфікса в порівняннях цілих чисел, синтаксис має ту саму структуру, яку ми вже бачили:

  • Для цілих чисел, p + cmp + умова + розмір (b, w, d або q).
  • Для чисел з плаваючою комою, cmp + p + розмір (s або d). Умову передають у безпосередньому значенні як додатковий операнд.
Порівняння цілих чисел

Як уже згадано, для цілих чисел є лише порівняння на рівність і на «більше ніж»:

інструкція опис
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq рівність для кожної доріжки
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq порівняння «більше ніж» для кожної доріжки зі знаком
movdqa  xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0

Щоб створити порівняння «менше ніж», можна використати gt з переставленими операндами: a < b == b > a.

Зауважмо, що порівняння враховує знак. Щоб виконати порівняння без знака, можна інвертувати старший біт обох операндів. Це можна зробити за допомогою XOR із маскою, де встановлено лише старший біт.

Note

Два корисні прийоми:

  1. XOR регістра із самим собою, щоб отримати всі нулі.
  2. Порівняння регістра із самим собою, щоб отримати всі одиниці.

Наприклад:

pxor xmm4, xmm4    ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones

Усі нулі й усі одиниці є поширеними масками, які кодують відповідно «хибно всюди» та «правдиво всюди». Їх також можна використати, щоб представити упакований 0 або упакований -1, які є поширеними сторожовими значеннями. Наприклад, NUL, який позначає кінець рядка тексту (англ. string), є 0.

Порівняння чисел з плаваючою комою

Доріжки чисел з плаваючою комою мають іншу форму: одна інструкція, cmpps (і cmppd для 64-бітних доріжок), з умовою як безпосереднім значенням:

movaps xmm0, [rel readings]
cmpps  xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]

NASM також має псевдооперації, які відповідають правильному безпосередньому значенню й легші для запамʼятовування. У всьому наведеному нижче x у px може бути s (32-бітні числа з плаваючою комою) або d (64-бітні числа з плаваючою комою):

псевдооперація безпосереднє значення порівняння
cmpeqpx 0 a == b
cmpltpx 1 a < b
cmplepx 2 a <= b
cmpunordpx 3 a є NaN або b є NaN
cmpneqpx 4 a != b
cmpnltpx 5 a >= b
cmpnlepx 6 a > b
cmpordpx 7 ні a, ні b не є NaN

Вибір за допомогою маски

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

; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0  ; xmm0 holds the mask, keep a copy
pand   xmm2, xmm3  ; xmm2 = a AND mask: lanes of a where mask is true
pandn  xmm0, xmm4  ; xmm0 = NOT mask AND b: lanes of b where mask is false
por    xmm2, xmm0  ; combine the two halves

Зауважмо, що асиметрія pandn тут дає перевагу: маска міститься в призначенні, заперечується й вибирає з b за одну інструкцію.

Ця закономірність є упакованою формою вибору без розгалужень. Обчислюють кожну доріжку, і сама лише маска вирішує, яке значення залишиться, без жодного jcc.

Спеціалізовані змішувальні інструкції

Є інструкції, які виконують той самий вибір безпосередньо, читаючи по одному біту на елемент із регістра маски. Їх називають змішувальними інструкціями:

інструкція елемент джерело маски
pblendvb байт неявний xmm0
blendvps 32-бітна доріжка неявний xmm0
blendvpd 64-бітна доріжка неявний xmm0

Зауважмо, що перша інструкція має синтаксис для цілих чисел, а дві інші — синтаксис для чисел з плаваючою комою. Однак, оскільки ці інструкції просто вибирають необроблені байти, будь-яку з них можна використати і з цілими числами, і з числами з плаваючою комою.

Для кожного елемента змішувальна інструкція залишає призначення, коли старший біт відповідного елемента маски дорівнює нулю, і бере джерело, коли він дорівнює одиниці. Враховують лише цей старший біт, чому відповідає маска порівняння, адже її доріжки складаються з самих одиниць або самих нулів. Регістр маски завжди xmm0, і він неявний:

movaps   xmm0, [rel mask]  ; the selecting mask must be in xmm0
movaps   xmm1, [rel b]     ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a]     ; source: taken where the mask bit is set

Також можна використати pblendvb, щоб вибрати доріжки з маски порівняння для будь-якого іншого розміру. Оскільки всі байти в правдивій доріжці є одиницями, pblendvb вибирає їх усі.

Note

Ці інструкції додають v після операції, яку виконують (blend). Це v означає змінна, бо вибір не статичний: він залежить від регістра.

Є також варіанти без v, які вибирають відповідно до безпосереднього значення. Вони дотримуються тієї самої закономірності: вибирають доріжку i, якщо біт i безпосереднього значення встановлено.

Від маски назад до скалярного коду

Хоча код SIMD і потужний, йому бракує значної частини гнучкості скалярного коду. У багатьох ситуаціях потрібно перейти від упакованого регістра назад у світ скалярних інструкцій.

Родина інструкцій movmsk править за міст між цими двома світами. Ці інструкції витягують старший біт кожної доріжки в регістр загального призначення:

інструкція збирає ширина результату
pmovmskb старший біт кожного з 16 байтів 16 бітів
movmskps старший біт кожного з 4 32-бітних слів 4 біти
movmskpd старший біт кожного з 2 64-бітних слів 2 біти

Якщо використати після порівняння, кожен встановлений біт представляє «правдиву» доріжку, а кожен скинутий біт — «хибну» доріжку. Потім цим результатом можна керувати як звично, скалярними інструкціями. Наприклад, popcnt рахує кількість збігів, а tzcnt знаходить перший.

Регістр загального призначення може бути 32-бітним або 64-бітним.

Перевірка цілого вектора

Існує також упакований варіант скалярної інструкції test: ptest.

Він подібний до свого скалярного відповідника тим, що виконує операцію AND між двома операндами, не змінюючи їх. На відміну від test, ptest також виконує операцію ANDN, заперечуючи перший операнд.

Отже, ptest можна розглядати як недеструктивну версію pand і pandn, яка встановлює прапорці відповідно до результату. Так само, як ці дві інструкції, ptest розглядає цілий SIMD-регістр як одну доріжку, тому не приймає префікса розміру.

Якщо результат операції AND дорівнює 0, встановлюється ZF, а якщо результат операції ANDN дорівнює 0, встановлюється CF. Це означає, що ptest можна використати, щоб перевірити і маску з самих одиниць, і маску з самих нулів:

  1. Використання ptest для регістра із самим собою встановлює ZF, лише якщо регістр складається з самих нулів. Це повторює поширений скалярний прийом: використати test для регістра із самим собою, щоб перевірити на 0.
  2. Використання ptest для регістра з маскою з самих одиниць встановлює CF, лише якщо регістр складається з самих одиниць. Крім того, воно встановлює ZF, лише якщо регістр складається з самих нулів, що дає змогу перевірити обидві маски водночас.
pxor    xmm0, xmm0   ; all zeros
pcmpeqb xmm1, xmm1   ; all ones
pcmpeqb xmm2, xmm2

ptest xmm0, xmm0     ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1     ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1     ; CF is set only if xmm2 is all ones

Результат ptest можна використати для розгалуження або в інструкціях без розгалужень, таких як setcc або cmovcc, як звично.

Вказівки

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

Кожен блок містить 4 результати, і станція застосовує ту саму операцію до кожного результату в блоці. Бал - 32-бітне число з плаваючою комою. Кілька кроків працюють з маскою: блоком із 4 доріжок, де кожна доріжка складається або з самих одиниць (так для цього результату), або з самих нулів (ні).

Перед нами пʼять завдань. Операнди ми отримуємо через адреси памʼяті. Деякі завдання записують свою відповідь за адресою результату, а інші повертають її безпосередньо.

Усі адреси памʼяті в цій вправі вирівняно на 16 байтів.

Note

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

1. Позначити бали, що перевищують поріг

Перший крок оцінює кожен результат щодо порогу. Результат зараховується, коли його бал строго більший за поріг. А якщо бал менший за поріг або дорівнює йому, то ні.

Реалізуймо функцію flag_above_threshold, яка будує маску: доріжка з самих одиниць для кожного балу, що перевищує свій поріг, і доріжка з самих нулів в іншому разі.

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

  • result: адреса памʼяті буфера, куди записуються 4 доріжки маски.
  • scores: адреса памʼяті балів, що містить 4 32-бітні нормальні числа з плаваючою комою (ніколи не NaN).
  • thresholds: адреса памʼяті порогу для кожної доріжки, що містить 4 32-бітні нормальні числа з плаваючою комою (ніколи не NaN).
scores     = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result     = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

Ця функція не має поверненого значення.

2. Позначити бездоганні бали

Окремий звіт виділяє бездоганні результати, ті, що досягли максимального можливого балу.

Реалізуймо функцію flag_perfect, яка будує маску: доріжка з самих одиниць для кожного балу, рівного своєму максимуму, і доріжка з самих нулів в іншому разі.

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

  • result: адреса памʼяті буфера, куди записуються 4 доріжки маски.
  • scores: адреса памʼяті балів, що містить 4 32-бітні нормальні числа з плаваючою комою (ніколи не NaN).
  • maxima: адреса памʼяті максимального балу для кожної доріжки, що містить 4 32-бітні нормальні числа з плаваючою комою (ніколи не NaN).
scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

Ця функція не має поверненого значення.

3. Присвоїти ранг

Кожен бал отримує ранг від 1 до 3:

  • Ранг 1 для балу, що дорівнює прохідному порогу 50.0 або нижчий за нього.
  • Ранг 2 для балу, вищого за цей поріг, але меншого за максимум.
  • Ранг 3 для бездоганного балу, того, що дорівнює максимуму.

Реалізуймо функцію assign_ranks, яка записує ранг кожного балу.

Прохідний поріг і значення рангів варто визначити як упаковані константи в памʼяті. Функції з двох попередніх завдань можна використати повторно: бал має щонайменше ранг 2, коли він вищий за поріг, і ранг 3, коли він дорівнює максимуму.

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

  • result: адреса памʼяті буфера, куди записуються 4 ранги, кожен - 32-бітне беззнакове ціле число.
  • scores: адреса памʼяті балів, що містить 4 32-бітні нормальні числа з плаваючою комою (ніколи не NaN).
  • maxima: адреса памʼяті максимального балу для кожної доріжки, що містить 4 32-бітні нормальні числа з плаваючою комою (ніколи не NaN).
scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}

Ця функція не має поверненого значення.

4. Порахувати провали

Упродовж року кожен учень накопичує підсумковий ранг. Станція підраховує, скільки рангів у всій когорті нижчі за прохідний поріг, щоб спланувати, скільки додаткових занять провести.

Реалізуймо функцію count_failures, яка повертає, скільки рангів у всіх блоках строго нижчі за прохідний поріг. Поріг подано як блок із 4 однакових доріжок, тож його можна завантажити один раз і повторно використовувати для кожного блоку.

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

  • ranks: адреса памʼяті рангів, ціле число блоків по 4 доріжки, кожен ранг - 32-бітне беззнакове ціле число.
  • block_count: кількість блоків по 4 доріжки, завжди більша за 0.
  • pass_threshold: адреса памʼяті прохідного порогу, що містить 4 однакові 32-бітні цілі числа.
ranks          = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count    = 2
pass_threshold = {2, 2, 2, 2}
// => 3

Ця функція повертає кількість як знакове 32-бітне ціле число.

5. Чи всі пройшли?

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

Реалізуймо функцію all_passed, яка повертає 1, якщо всі учні пройшли, і 0 в іншому разі. Учень проходить, коли відповідна доріжка в масиві failing містить самі нулі.

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

  • failing: адреса памʼяті масок провалу, ціле число блоків по 4 доріжки, кожна доріжка - самі одиниці або самі нулі.
  • block_count: кількість блоків по 4 доріжки, завжди більша за 0.
failing     = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
               0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1

Ця функція повертає відповідь як знакове 32-бітне ціле число: 1 або 0.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
x86-64 Assembly Exercism

Час розпочати Відомість оцінок?

Зареєструйтеся на Exercism, щоб вивчати й опановувати x86-64 Assembly, а також 22 концепції130 вправ та справжнє наставництво від людей, і все це безкоштовно.