У попередньому концепті ми вже розглядали упаковані значення з плаваючою комою: кілька чисел в одному регістрі xmm, з якими операції виконують по доріжках і паралельно.
Ті самі регістри xmm можуть також зберігати упаковані цілі числа.
Більша частина моделі SIMD для чисел з плаваючою комою переноситься на упаковані цілі числа без змін:
Однак синтаксис трохи інший:
p, що вказує: інструкція працює з упакованими даними.add, mul тощо).Упаковані цілі числа мають чотири розміри доріжок, і кожен має власний суфікс:
| ширина доріжки | байти | доріжок у 128 бітах | суфікс |
|---|---|---|---|
| байт | 1 | 16 | b |
| слово | 2 | 8 | w |
| подвійне слово | 4 | 4 | d |
| четверне слово | 8 | 2 | q |
Наприклад:
| інструкція | що означає |
|---|---|
paddb |
упакований add, 8-бітні доріжки (16 доріжок) |
paddw |
упакований add, 16-бітні доріжки (8 доріжок) |
paddd |
упакований add, 32-бітні доріжки (4 доріжки) |
paddq |
упакований add, 64-бітні доріжки (2 доріжки) |
Деякі інструкції приймають доріжки одного розміру на вході, але іншого розміру на виході. Вони дотримуються тієї самої загальної умовності, але з двома суфіксами розміру. Перший указує розмір вхідної доріжки, другий указує розмір вихідної доріжки:
| інструкція | що означає |
|---|---|
pmovsxwd |
упакований movsx, з 16-бітних доріжок у 32-бітні доріжки |
pmuldq |
упакований mul, з 32-бітних доріжок у 64-бітні доріжки |
Дві інструкції з «цілочисловими» назвами копіюють 128 біт між регістром xmm і памʼяттю:
| інструкція | опис |
|---|---|
movdqa |
копіює упаковані цілі числа до вирівняного розташування або з нього |
movdqu |
копіює упаковані цілі числа до невирівняного розташування або з нього |
Вони поводяться як movaps і movups: movdqa спричиняє помилку на невирівняній адресі, а movdqu приймає будь-яку.
Усі чотири копіюють 128 біт, не тлумачачи їх.
Пару з «цілочисловими» назвами використовують із цілочисловими даними за домовленістю, а не за вимогою.
dq тут означає double-qword, тобто 128 біт (16 байт).
Додавання і віднімання дотримуються правила найменування:
paddb xmm0, xmm1 ; 16 lanes: each 8-bit, xmm0 += xmm1
paddw xmm2, xmm3 ; 8 lanes: each 16-bit, xmm2 += xmm3
psubd xmm4, xmm5 ; 4 lanes: each 32-bit, xmm4 -= xmm5
psubq xmm6, xmm7 ; 2 lanes: each 64-bit, xmm6 -= xmm7
Окремої форми зі знаком і без знака немає.
У доповняльному коді додавання й віднімання дають ті самі біти незалежно від того, читають доріжки як числа зі знаком чи без знака, тож одна інструкція обслуговує обидва випадки.
Тлумачення залишається за нами, точно як зі скалярними add і sub.
Ці інструкції загортаються при переповненні, як і їхні скалярні відповідники.
8-бітна доріжка зберігає значення за модулем 256, тож paddb від 200 + 100 дає 300 - 256 = 44, відкидаючи біти, які не вміщуються.
Зауважмо, що ці зайві біти не переносяться в наступну доріжку. З кожною доріжкою працюють окремо від інших, навіть попри те, що вони ділять один регістр.
SIMD для цілих чисел додає операцію, якої немає в SIMD для чисел з плаваючою комою: додавання і віднімання з насиченням, які обмежують значення замість загортання. Результат, більший за діапазон доріжки, стає найбільшим значенням, яке доріжка може вмістити; результат, менший за діапазон, стає найменшим.
Форми з насиченням вставляють s (зі знаком) або us (без знака) перед суфіксом розміру:
| інструкція | що означає |
|---|---|
paddsb |
add, з насиченням, 8-бітні доріжки зі знаком |
paddusb |
add, з насиченням, 8-бітні доріжки без знака |
psubsw |
sub, з насиченням, 16-бітні доріжки зі знаком |
psubusw |
sub, з насиченням, 16-бітні доріжки без знака |
Діапазон обмеження - повний діапазон, який можна подати цілим числом відповідного розміру й знаковості. Для байта:
[0, 255]: paddusb від 200 + 100 дає 255, а psubusb від 5 - 10 дає 0.[-128, 127]: paddsb від 100 + 50 дає 127.Насичення важливе, коли доріжка зберігає обмежену величину, наприклад канал пікселя або звуковий семпл. Загортання зробило б надто яскравий піксель темним, тоді як обмеження зберігає його на максимальній яскравості, а це саме той результат, який нам потрібен.
Додавання і віднімання з насиченням існують лише для доріжок байтів і слів, а не для подвійних чи четверних слів.
Множення двох N-бітних значень може дати 2N-бітний добуток, але доріжка призначення має ширину лише N біт. Операції SIMD розвʼязують це, вказуючи, яку половину добутку зберегти: молодші N біт або старші N біт.
Для 16-бітних доріжок використовують три інструкції:
| інструкція | що означає |
|---|---|
pmullw |
mul, 16-бітні доріжки, зберегти молодші 16 біт кожного добутку |
pmulhw |
mul, 16-бітні доріжки, зберегти старші 16 біт, операнди зі знаком |
pmulhuw |
mul, 16-бітні доріжки, зберегти старші 16 біт, операнди без знака |
Молодші 16 біт добутку однакові незалежно від того, читають операнди як числа зі знаком чи без знака, тож існує єдина інструкція pmullw.
Однак старші 16 біт різняться залежно від знаковості результату. Саме тому множення старшої половини має окремі форми для множення зі знаком і без знака:
pmulhw для множення зі знаком.pmulhuw, з додатковою u, для множення без знака.Звернімо увагу на синтаксис:
p для упакованих цілих чисел.mul.h, щоб указати, що вибрано старші («high») біти результату.u, якщо результат має тлумачитися як число без знака (тобто його не розширено знаком).w, який указує, що операція виконується над словом (16 біт).Упаковане множення для слів точно дотримується наведеного правила.
Множення подвійних слів (32-бітних) теж дотримується правила, але існує лише варіант, який вибирає молодшу половину: pmulld.
Немає варіанта множення подвійних слів, який вибирає старші біти. Однак є варіанти, які розширюють множення, зберігаючи повний 64-бітний добуток доріжок з парними індексами (тобто доріжок у позиціях 0 і 2):
| інструкція | що означає |
|---|---|
pmuludq |
перемножити 32-бітні елементи з парними індексами, без знака, у 2 повні 64-бітні добутки |
pmuldq |
перемножити 32-бітні елементи з парними індексами, зі знаком, у 2 повні 64-бітні добутки |
Зауважмо, що в синтаксисі використано dq, можливо, з u перед ним для множення без знака.
Це тому, що інструкції приймають доріжки dword і видають доріжки qword.
Упакованого ділення цілих чисел не існує. Код, якому воно потрібне, перетворює значення на числа з плаваючою комою, ділить і перетворює назад.
Існують упаковані відповідники movsx і movzx.
Вони дотримуються того самого синтаксису, який ми згадували для інструкцій, що приймають вхідні дані з іншим розміром доріжки, ніж їхні вихідні дані:
pmovsxwd xmm0, xmm1 ; 4 words -> 4 dwords, sign-extended
pmovzxbw xmm0, xmm1 ; 8 bytes -> 8 words, zero-extended
Зауважмо, що кількість доріжок задає більша (вихідна) ширина.
Інструкція читає стільки доріжок із молодшої частини джерела.
Це та сама семантика, яку ми вже бачили для упакованих інструкцій cvt між числами з плаваючою комою одинарної та подвійної точності.
Існують інструкції для перетворення між 32-бітними цілими числами зі знаком і доріжками чисел з плаваючою комою.
Вони дотримуються звичайного синтаксису інструкцій cvt і cvtt, але замість si упаковане ціле число позначають як dq:
cvtdq2ps xmm0, xmm1 ; convert 32-bit signed integers in xmm1 to 32-bit floats in xmm0
cvtdq2pd xmm2, xmm3 ; convert 32-bit signed integers in xmm3 to 64-bit floats in xmm2
cvtps2dq xmm4, xmm5 ; convert 32-bit floats in xmm5 to 32-bit signed integers in xmm4
Інструкції, які використовують pi для позначення упакованих цілих чисел, пишуть у застарілі регістри mmx і фактично вважаються застарілими на x86-64.
Надавайте перевагу формам із dq, які використовують регістри xmm.
Ті самі зауваження, що й для скалярного перетворення між числами з плаваючою комою і цілими числами, слушні й тут.
Числа з плаваючою комою округлюються згідно зі спеціальним регістром MXCSR, режим якого не можна вважати наперед відомим при вході у функцію.
Існує варіант cvtt (з додатковою t), який завжди обрізає результат.
Також можна використати round, щоб привести упаковані значення з плаваючою комою до відомого стану перед перетворенням.
Керівне значення для round те саме, що й для скалярного round, і синтаксис для упакованих значень з плаваючою комою теж той самий:
roundps xmm0, xmm1, 1 ; xmm0 = floor(xmm1), packed 32-bit floats
roundpd xmm2, xmm3, 2 ; xmm2 = ceil(xmm3), packed 64-bit floats
Ми пишемо цифрове ядро невеликого мікшерного пульта в студії звукозапису.
Звук надходить як потік семплів. Кожен семпл - це число, що описує положення дифузора гучномовця в певний момент. Пульт обробляє цілий блок семплів за раз, застосовуючи ту саму операцію до кожного.
На нас чекає пʼять завдань, кожне з яких працює з блоком семплів, що зберігається в памʼяті. Операнди ми отримуємо через адреси памʼяті, а відповідь записуємо через адресу результату.
Обчислення у цій вправі слід виконувати за допомогою інструкцій SIMD, а не скалярних операцій.
Іноді студія записує інструменти на різні доріжки. Щоб відтворити їх через ті самі колонки, ми додаємо дві доріжки в одну.
Реалізуйте функцію mix_tracks, яка додає дві доріжки семпл за семплом.
Кожна доріжка - це блок із 8 знакових 16-бітних семплів.
Сума, яка виходить за межі діапазону знакових 16-бітних чисел, повинна насичуватися, а не загортатися.
Ця функція приймає як аргументи, у такому порядку:
result: адреса памʼяті, вирівняна на 16 байтів, для буфера, куди записуються 8 змішаних семплів.track_a: адреса памʼяті першої доріжки, вирівняна на 16 байтів, із 8 знаковими 16-бітними семплами.track_b: адреса памʼяті другої доріжки, вирівняна на 16 байтів, із 8 знаковими 16-бітними семплами.track_a = {10000, 30000, 20000, -30000, -20000, -100, 100, 0}
track_b = { 5000, 5000, 20000, -5000, -20000, 100, -100, 0}
result = {15000, 32767, 32767, -32768, -32768, 0, 0, 0}
Ця функція не повертає значення.
Коли два мікрофони записують одночасно, кожен із них уловлює слабку копію джерела іншого. Цю небажану копію називають просочуванням. Щойно це просочування виміряно, його потрібно відняти, щоб очистити доріжку.
Реалізуйте функцію remove_bleed, яка віднімає блок просочування від блоку доріжки, семпл за семплом.
Кожен блок - це 8 знакових 16-бітних семплів, і віднімання має насичуватися.
Ця функція приймає як аргументи, у такому порядку:
result: адреса памʼяті, куди записуються 8 очищених семплів (вирівнювання не гарантовано).track: адреса памʼяті записаної доріжки, вирівняна на 16 байтів, 8 знакових 16-бітних семплів.bleed: адреса памʼяті просочування, яке треба прибрати, 8 знакових 16-бітних семплів (вирівнювання не гарантовано).track = {10000, -30000, 5000, 32767, 0, 200, -200, 15000}
bleed = { 2000, 5000, 5000, -5, 0, 300, 300, 5000}
result = { 8000, -32768, 0, 32767, 0, -100, -500, 10000}
Ця функція не повертає значення.
Над кожним каналом розташований індикатор рівня, невеличка смужка, яскравість якої показує, наскільки гучний цей канал. На передній панелі є один головний індикатор, який має світитися так само яскраво, як два канали разом. Ми додаємо два блоки індикаторів, щоб керувати ним.
Звернімо увагу, що яскравий індикатор залишається на повній яскравості, а не загортається назад до темного, тож сума насичується.
Реалізуйте функцію combine_meters, яка додає два блоки індикаторів семпл за семплом.
Ця функція приймає як аргументи, у такому порядку:
result: адреса памʼяті, вирівняна на 16 байтів, куди записуються 16 обʼєднаних значень.meter_a: адреса памʼяті блоку індикатора першого каналу, вирівняна на 16 байтів, із 16 упакованими беззнаковими 8-бітними значеннями.meter_b: адреса памʼяті блоку індикатора другого каналу, вирівняна на 16 байтів, із 16 упакованими беззнаковими 8-бітними значеннями.Кожен блок - це 16 беззнакових 8-бітних значень. Їх треба додати, насичуючи до беззнакового діапазону.
meter_a = {200, 100, 50, 255, 0, 128, 10, 240, 1, 2, 3, 4, 5, 6, 7, 8}
meter_b = {100, 100, 200, 1, 0, 128, 10, 20, 1, 2, 3, 4, 5, 6, 7, 8}
result = {255, 200, 250, 255, 0, 255, 20, 255, 2, 4, 6, 8, 10, 12, 14, 16}
Ця функція не повертає значення.
Пісня не повинна обриватися різко, а має плавно затухати в кінці. У нас є блок коефіцієнтів підсилення, по одному на кожен семпл, які дедалі зменшуються.
Кожен коефіцієнт - це дріб, виражений у частках від 65536.
Наприклад, якщо семпл має значення 16384, він насправді виражає 16384 / 65536, тобто 1 / 4.
Найбільше придатне значення, 32767, трохи менше за половину.
Оскільки 65536 - це 2^16, множення семплу на його коефіцієнт зі збереженням старшої половини добутку дає sample * gain / 65536.
Реалізуйте функцію apply_fade, яка масштабує кожен семпл на його коефіцієнт.
Ця функція приймає як аргументи, у такому порядку:
result: адреса памʼяті, вирівняна на 16 байтів, куди записуються всі масштабовані семпли.track: адреса памʼяті доріжки, вирівняна на 16 байтів, із n знаковими 16-бітними семплами.gains: адреса памʼяті коефіцієнтів підсилення, вирівняна на 16 байтів, із n знаковими 16-бітними значеннями, кожне в діапазоні від 0 до 32767.n: кількість семплів, яка завжди відмінна від нуля й кратна 8, як 64-бітне беззнакове ціле число.Звернімо увагу, що доріжка - це масив, який містить n знакових 16-бітних семплів, де n ніколи не дорівнює нулю й завжди кратне 8.
Для кожного семплу масштабований результат - це старші 16 бітів знакового добутку: sample * gain.
track = {20000, 20000, 20000, 20000, -20000, -20000, -20000, -20000}
gains = {32767, 24576, 16384, 8192, 32767, 24576, 16384, 8192}
result = { 9999, 7500, 5000, 2500, -10000, -7500, -5000, -2500}
Збереження старшої половини округлює в напрямку мінус нескінченності.
Ось чому 20000 * 32767 масштабується до 9999, а -20000 * 32767 масштабується до -10000, а не до -9999.
Ця функція не повертає значення.
Барабанщика записали занадто гучно. Щоб знизити рівень усієї доріжки, ми ділимо кожен семпл на константу.
Реалізуйте функцію attenuate_track.
Ця функція приймає як аргументи, у такому порядку:
result: адреса памʼяті, вирівняна на 16 байтів, куди записуються результати як 32-бітні знакові цілі числа.samples: адреса памʼяті семплів, вирівняна на 16 байтів, n знакових 16-бітних значень.divisor: адреса памʼяті 4 однакових копій дільника, вирівняна на 16 байтів, як 32-бітні цілі числа.n - це кількість семплів, яка завжди відмінна від нуля й кратна 4, як 64-бітне беззнакове ціле число.Звернімо увагу, що samples - це масив, який містить n знакових 16-бітних семплів, де n ніколи не дорівнює нулю й завжди кратне 4.
Дільник подається як 4 копії одного й того самого 32-бітного цілого числа, уже розмножені, тож ним можна одразу заповнити всі чотири смуги.
У цілочисельному SIMD немає упакованого ділення, тож роботу виконують в упакованій плаваючій комі, а потім перетворюють назад на упаковані 32-бітні цілі числа, щоб зберегти в result.
Перетворюйте назад, відкидаючи дробову частину в напрямку нуля, тож частка 2.3 стає 2, а -2.3 перетворюється на -2.
samples = {300, 150, 90, -120, 100, 50, -50, 7}
divisor = {3, 3, 3, 3}
result = {100, 50, 30, -40, 33, 16, -16, 2}
Ця функція не повертає значення.
Зареєструйтеся на Exercism, щоб вивчати й опановувати x86-64 Assembly, а також 22 концепції130 вправ та справжнє наставництво від людей, і все це безкоштовно.