Треки
/
x86-64 Assembly
x86-64 Assembly
/
Вправи
/
Мікшерний пульт
Мікшерний пульт

Мікшерний пульт

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

Вступ

SIMD: цілі числа

У попередньому концепті ми вже розглядали упаковані значення з плаваючою комою: кілька чисел в одному регістрі xmm, з якими операції виконують по доріжках і паралельно. Ті самі регістри xmm можуть також зберігати упаковані цілі числа.

Синтаксис

Більша частина моделі SIMD для чисел з плаваючою комою переноситься на упаковані цілі числа без змін:

  • 128-бітний регістр поділено на доріжки
  • Інструкції діють паралельно на доріжки, розташовані в однакових позиціях
  • Операнди памʼяті підкоряються тим самим правилам вирівнювання на 16 байт

Однак синтаксис трохи інший:

  1. Префікс p, що вказує: інструкція працює з упакованими даними.
  2. Операція, яку виконують, з тією самою назвою, що й у її не-SIMD (її ще називають скалярною) відповідника (наприклад, add, mul тощо).
  3. Суфікс, що вказує розмір кожної доріжки.

Упаковані цілі числа мають чотири розміри доріжок, і кожен має власний суфікс:

ширина доріжки байти доріжок у 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 біт, не тлумачачи їх. Пару з «цілочисловими» назвами використовують із цілочисловими даними за домовленістю, а не за вимогою.

Note

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.

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

Note

Додавання і віднімання з насиченням існують лише для доріжок байтів і слів, а не для подвійних чи четверних слів.

Множення

Множення двох N-бітних значень може дати 2N-бітний добуток, але доріжка призначення має ширину лише N біт. Операції SIMD розвʼязують це, вказуючи, яку половину добутку зберегти: молодші N біт або старші N біт.

Для 16-бітних доріжок використовують три інструкції:

інструкція що означає
pmullw mul, 16-бітні доріжки, зберегти молодші 16 біт кожного добутку
pmulhw mul, 16-бітні доріжки, зберегти старші 16 біт, операнди зі знаком
pmulhuw mul, 16-бітні доріжки, зберегти старші 16 біт, операнди без знака

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

Однак старші 16 біт різняться залежно від знаковості результату. Саме тому множення старшої половини має окремі форми для множення зі знаком і без знака:

  1. pmulhw для множення зі знаком.
  2. pmulhuw, з додатковою u, для множення без знака.

Звернімо увагу на синтаксис:

  1. p для упакованих цілих чисел.
  2. Операція, яку виконують: mul.
  3. h, щоб указати, що вибрано старші («high») біти результату.
  4. Необовʼязкова u, якщо результат має тлумачитися як число без знака (тобто його не розширено знаком).
  5. І нарешті, суфікс розміру 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
Caution

Інструкції, які використовують 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

Вказівки

Ми пишемо цифрове ядро невеликого мікшерного пульта в студії звукозапису.

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

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

Note

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

1. Змішати два треки

Іноді студія записує інструменти на різні доріжки. Щоб відтворити їх через ті самі колонки, ми додаємо дві доріжки в одну.

Реалізуйте функцію 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}

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

2. Прибрати просочування

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

Реалізуйте функцію 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}

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

3. Обʼєднати індикатори

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

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

Реалізуйте функцію 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}

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

4. Застосувати затухання

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

Кожен коефіцієнт - це дріб, виражений у частках від 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}
Note

Збереження старшої половини округлює в напрямку мінус нескінченності. Ось чому 20000 * 32767 масштабується до 9999, а -20000 * 32767 масштабується до -10000, а не до -9999.

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

5. Послабити доріжку

Барабанщика записали занадто гучно. Щоб знизити рівень усієї доріжки, ми ділимо кожен семпл на константу.

Реалізуйте функцію 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}

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

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

Час розпочати Мікшерний пульт?

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