Скалярний код покладається на прапорці, які встановлюють різні інструкції, щоб розгалузитися у відповідь на певну умову. Упаковані значення, однак, представляють не одне, а багато значень паралельно. Одна умова може не виконуватися для однієї доріжки й виконуватися для іншої.
Ось чому код 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 із маскою, де встановлено лише старший біт.
Два корисні прийоми:
Наприклад:
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 вибирає їх усі.
Ці інструкції додають 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 можна використати, щоб перевірити і маску з самих одиниць, і маску з самих нулів:
ptest для регістра із самим собою встановлює ZF, лише якщо регістр складається з самих нулів.
Це повторює поширений скалярний прийом: використати test для регістра із самим собою, щоб перевірити на 0.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 байтів.
Обчислення в цій вправі слід виконувати за допомогою інструкцій SIMD.
Перший крок оцінює кожен результат щодо порогу. Результат зараховується, коли його бал строго більший за поріг. А якщо бал менший за поріг або дорівнює йому, то ні.
Реалізуймо функцію 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}
Ця функція не має поверненого значення.
Окремий звіт виділяє бездоганні результати, ті, що досягли максимального можливого балу.
Реалізуймо функцію 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}
Ця функція не має поверненого значення.
Кожен бал отримує ранг від 1 до 3:
50.0 або нижчий за нього.Реалізуймо функцію 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}
Ця функція не має поверненого значення.
Упродовж року кожен учень накопичує підсумковий ранг. Станція підраховує, скільки рангів у всій когорті нижчі за прохідний поріг, щоб спланувати, скільки додаткових занять провести.
Реалізуймо функцію 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-бітне ціле число.
Перш ніж підшити записи до архіву, станція перевіряє, чи когорта чиста: вона проходить, коли в жодному блоці не провалився жоден результат.
Реалізуймо функцію 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.
Зареєструйтеся на Exercism, щоб вивчати й опановувати x86-64 Assembly, а також 22 концепції130 вправ та справжнє наставництво від людей, і все це безкоштовно.