Сложение с помощью инструкций SSE/AVX

Последнее обновление: 19.10.2023

Для сложения соответствующих элементов векторов применяются следующие инструкции:

  • paddb: сложение байтов в 16 дорожках

  • vpaddb: сложение байтов в 16 дорожках (для 128-битной версии) и в 32 дорожках (для 256-битной версии)

  • paddw: сложение слов (значений .short/.word) в 8 дорожках

  • vpaddw: сложение слов в 8 дорожках (для 128-битной версии) и в 16 дорожках (для 256-битной версии)

  • paddd: сложение двойных слов (значений .long) в 4 дорожках

  • vpaddd: сложение двойных слов в 4 дорожках (для 128-битной версии) и в 8 дорожках (для 256-битной версии)

  • paddq: сложение четверных слов (тип .quad) в 2 дорожках

  • vpaddq: сложение четверных слов в 2 дорожках (128-битной версии) и в 4 дорожках (для 256-битной версии)

Синтаксис инструкций:

paddb xmmdest, xmmsrc/mem128 
vpaddb xmmdest, xmmsrc1, xmmsrc2/mem128 
vpaddb ymmdest, ymmsrc1, ymmsrc2/mem256

paddw xmmdest, xmmsrc/mem128
vpaddw xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddw ymmdest, ymmsrc1, ymmsrc2/mem256

paddd xmmdest, xmmsrc/mem128
vpaddd xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddd ymmdest, ymmsrc1, ymmsrc2/mem256

paddq xmmdest, xmmsrc/mem128
vpaddq xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddq ymmdest, ymmsrc1, ymmsrc2/mem256

Инструкции с двумя операндами складывают соответствующие дорожки двух операндов и результат помещают в первый операнд. Инструкции с тремя операндами складывают второй и третий операнды и результат помещают в первый. Пример сложения в программе на Linux:

global main

extern printf

section .data
nums0 dd 1, 2, 4, 8
nums1 dd 2, 3, 5, 9
       
format_str db "%d, %d, %d, %d", 10, 0

section .text
main: 
    sub rsp, 8

    movaps xmm0, [nums0]
    movaps xmm1, [nums1]
    paddd xmm0, xmm1       ; XMM0 = XMM0 + XMM1 

    movd esi, xmm0
    psrldq xmm0, 4  ; сдвиг вправо в XMM0 для получения следующего числа
    movd edx, xmm0
    psrldq xmm0, 4
    movd ecx, xmm0
    psrldq xmm0, 4
    movd r8d, xmm0
    mov rdi, format_str
    call printf

    add rsp, 8
    ret

В данном случае в регистре XMM0 окажется вектор, элементы которого представляют сумму соответствующих дорожек двух регистров, то есть вектор 3, 5, 9, 17. После сложения для проверки выводим вектор на консоль. Пример компиляции и выполнения программы:

root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o
root@Eugene:~/asm# gcc -static  hello.o -o hello
root@Eugene:~/asm# ./hello
3, 5, 9, 17
root@Eugene:~/asm#

Аналогичный пример на Windows:

global main

extern printf

section .data
nums0 dd 1, 2, 4, 8
nums1 dd 2, 3, 5, 9
format_str db "%d, %d, %d, %d", 10, 0

section .text
main: 
    sub rsp, 8
    movaps xmm0, [rel nums0]     ; вектор num0 в регистр xmm0
    movaps xmm1, [rel nums1]     ; вектор num1 в регистр xmm1
    paddd xmm0, xmm1       ; XMM0 = XMM0 + XMM1 
    
    movd edx, xmm0        ; помещаем первое число в rdx
    psrldq xmm0, 4        ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0
    movd r8d, xmm0        ; помещаем первое число в r8
    psrldq xmm0, 4        ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0
    movd r9d, xmm0        ; помещаем первое число в r9
    psrldq xmm0, 4        ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0
    movd [rsp+32], xmm0        ; помещаем первое число в стек

    mov rcx, format_str
    call printf

    add rsp, 40
    ret

Эти инструкции не влияют ни на какие флаги и, таким образом, не указывают, когда во время выполнения этих инструкций происходит переполнение (со знаком или без знака). Если перенос происходит во время добавления, перенос теряется. Это может привести к неправильным результатам при рассчетах, например:

global main

extern printf

section .data
nums0 dw -32768, 0, 0, 0, 0, 0, 0, 0
nums1 dw -10, 0, 0, 0, 0, 0, 0, 0
       
format_str db "%d", 10, 0

section .text
main: 
    sub rsp, 8

    movaps xmm0, [nums0]
    movaps xmm1, [nums1]
    paddw xmm0, xmm1       ; XMM0 = XMM0 + XMM1 

    movd esi, xmm0
    movsx esi, si         ; расширение знаком 16-рарядного числа до 32 разрядов
    mov rdi, format_str
    call printf

    add rsp, 8
    ret

Здесь складываем два вектора и для демонстрации проблемы выводим первый элемент результирующего вектора. Первый элемент вектора nums0 (-32768) складывается с первым элементом вектора nums1 (-10), результатом будет число 32758. Определенно это не тот результат, который мы бы хотели видеть. Пример работы программы:

root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o
root@Eugene:~/asm# gcc -static  hello.o -o hello
root@Eugene:~/asm# ./hello
32758
root@Eugene:~/asm#

Программа сама должна проверять, что складываемые операнды находятся в соответствующем диапазоне. Но дополнительно SSE/AVX предоставляют инструкции, которые используют так называемую "арифметику насыщения" (saturation arithmetic). Арифметика насыщения хорошо работает для обработки мультимедиа - аудио, видео, изображений. Это следующие инструкции:

  • paddsb: сложение байтов со знаком в 16 дорожках

  • vpaddsb: сложение байтов со знаком в 16 дорожках

  • vpaddsb: сложение байтов со знаком в 32 дорожках

  • paddsw: сложение слов со знаком в 8 дорожках

  • vpaddsw: сложение слов со знаком в 8 дорожках

  • vpaddsw: сложение слов со знаком в 16 дорожках

  • paddusb: сложение беззнаковых байтов в 16 дорожках

  • vpaddusb: сложение беззнаковых байтов в 16 дорожках

  • vpaddusb: сложение беззнаковых байтов в 32 дорожках

  • paddusw: сложение беззнаковых слов в 8 дорожках

  • vpaddusw: сложение беззнаковых слов в 8 дорожках

  • vpaddusw: сложение беззнаковых слов в 16 дорожках

Синтаксис инструкций:

paddsb xmmdest, xmmsrc/mem128
vpaddsb xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddsb ymmdest, ymmsrc1, ymmsrc2/mem256

paddsw xmmdest, xmmsrc/mem128
vpaddsw xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddsw ymmdest, ymmsrc1, ymmsrc2/mem256

paddusb xmmdest, xmmsrc/mem128
vpaddusb xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddusb ymmdest, ymmsrc1, ymmsrc2/mem256

paddusw xmmdest, xmmsrc/mem128
vpaddusw xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddusw ymmdest, ymmsrc1, ymmsrc2/mem256

Для сложения без знака переполнение усекается до максимально возможного значения, которое может выдержать размер инструкции. Например, если сложение двух байтовых значений превышает 0xFF, арифметика насыщения дает 0xFF — максимально возможное 8-битное значение без знака. Аналогично, если при вычитании произойдет потеря значимости (underflow), то результат округляется до 0. Для арифметики насыщения со знаком отсечение происходит при наибольшем положительном и наименьшем отрицательном значениях (например, для чисел размером с 1 байт это 0x7F/+127 для положительных значений и 0x80/–128 для отрицательных значений).

Так, изменим предыдущий пример, применив сложение чисел со знаком:

global main

extern printf

section .data
nums0 dw -32768, 2, -4, 8, -16, 32, -64, 128
nums1 dw -10, -3, 5, -9, 15, -31, 55, -112
       
format_str db "%d", 10, 0

section .text
main: 
    sub rsp, 8

    movaps xmm0, [nums0]
    movaps xmm1, [nums1]
    paddsw xmm0, xmm1       ; XMM0 = XMM0 + XMM1 
    ; XMM0 = -32768, -1, 1, -1, -1, 1, -9, 16
    movd esi, xmm0
    movsx esi, si         ; расширение знаком 16-рарядного числа до 32 разрядов
    mov rdi, format_str
    call printf

    add rsp, 8
    ret

В данном случае результат сложения первых элементов векторов - -32768 + -10 выходит за минимальные границы диапазона типа sword, но теперь минимальное значение - -32768, что, возможно, в каких ситуациях может быть приемлимым результатом.

root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o
root@Eugene:~/asm# gcc -static  hello.o -o hello
root@Eugene:~/asm# ./hello
-32768
root@Eugene:~/asm#

Горизонтальное сложение

Расширения e SSE/AVX также предоставляют инструкции для так называемого "горизонтального сложения":

  • (v)phaddw: сложение 16-битных чисел

  • (v)phaddd: сложение 32-битных чисел

  • (v)phaddsw: сложение 16-битных чисел с насыщением

Они имеют аналогичный синтаксис:

phaddw xmmdest, xmmsrc/mem128
vphaddw xmmdest, xmmsrc1, xmmsrc2/mem128
vphaddw ymmdest, ymmsrc1, ymmsrc2/mem256

phaddd xmmdest, xmmsrc/mem128
vphaddd xmmdest, xmmsrc1, xmmsrc2/mem128
vphaddd ymmdest, ymmsrc1, ymmsrc2/mem256

phaddsw xmmdest, xmmsrc/mem128
vphaddsw xmmdest, xmmsrc1, xmmsrc2/mem128
vphaddsw ymmdest, ymmsrc1, ymmsrc2/mem256

Инструкции горизонтального сложения складывают соседние слова или двойные слова обоих операндов и сохраняют сумму в дорожке регистра из первого операнда. В случае с инструкцией phaddw сложение будет выполняться следующим образом:

temp[0-15] = xmmdest[0-15] + xmmdest[16-31]
temp[16-31] = xmmdest[32-47] + xmmdest[48-63]
temp[32-47] = xmmdest[64-79] + xmmdest[80-95]
temp[48-63] = xmmdest[96-111] + xmmdest[112-127]
temp[64-79] = xmmsrc/mem128[0-15] + xmmsrc/mem128[16-31]
temp[80-95] = xmmsrc/mem128[32-47] + xmmsrc/mem128[48-63]
temp[96-111] = xmmsrc/mem128[64-79] + xmmsrc/mem128[80-95]
temp[112-127] = xmmsrc/mem128[96-111] + xmmsrc/mem128[112-127]
xmmdest = temp

4 слова из младших 64 битов результата являются суммой соседних слов первого операнда, а 4 слова из старших 64 бит результата - сумма соседних слов из второго операнда. Инструкция phaddw не затрагивает старшие 128 бит перекрывающего регистра YMM.

Инструкция vphaddw складывает слова из второго и третьего операнда и результат помещает в первый. При этом старшие 128 бит перекрывающего регистра YMM заполняются нулями:

xmmdest[0-15] = xmmsrc1[0-15] + xmmsrc1[16-31]
xmmdest[16-31] = xmmsrc1[32-47] + xmmsrc1[48-63]
xmmdest[32-47] = xmmsrc1[64-79] + xmmsrc1[80-95]
xmmdest[48-63] = xmmsrc1[96-111] + xmmsrc1[112-127]
xmmdest[64-79] = xmmsrc2/mem128[0-15] + xmmsrc2/mem128[16-31]
xmmdest[80-95] = xmmsrc2/mem128[32-47] + xmmsrc2/mem128[48-63]
xmmdest[96-111] = xmmsrc2/mem128[64-79] + xmmsrc2/mem128[80-95]
xmmdest[111-127] = xmmsrc2/mem128[96-111] + xmmsrc2/mem128[112-127]

256-разрядная версия инструкции vphaddw выполняет вычисления следующим образом:

ymmdest[0-15] = ymmsrc1[16-31] + ymmsrc1[0-15]
ymmdest[16-31] = ymmsrc1[48-63] + ymmsrc1[32-47]
ymmdest[32-47] = ymmsrc1[80-95] + ymmsrc1[64-79]
ymmdest[48-63] = ymmsrc1[112-127] + ymmsrc1[96-111]
ymmdest[64-79] = ymmsrc2[16-31] + ymmsrc2[0-15]
ymmdest[80-95] = ymmsrc2[48-63] + ymmsrc2[32-47]
ymmdest[96-111] = ymmsrc2[80-95] + ymmsrc2[64-79]
ymmdest[112-127] = ymmsrc2[112-127] + ymmsrc2[96-111]
ymmdest[128-143] = ymmsrc1[144-159] + ymmsrc1[128-143]
ymmdest[144-159] = ymmsrc1[176-191] + ymmsrc1[160-175]
ymmdest[160-175] = ymmsrc1[208-223] + ymmsrc1[192-207]
ymmdest[176-191] = ymmsrc1[240-255] + ymmsrc1[224-239]
ymmdest[192-207] = ymmsrc2[144-159] + ymmsrc2[128-143]
ymmdest[208-223] = ymmsrc2[176-191] + ymmsrc2[160-175]
ymmdest[224-239] = ymmsrc2[208-223] + ymmsrc2[192-207]
ymmdest[240-255] = ymmsrc2[240-255] + ymmsrc2[224-239]

Горизонтальное сложение двойных слов с помощью инструкции phaddd:

temp[0-31] = xmmdest[0-31] + xmmdest[32-63]
temp[32-63] = xmmdest[64-95] + xmmdest[96-127]
temp[64-95] = xmmsrc/mem128[0-31] + xmmsrc/mem128[32-63]
temp[96-127] = xmmsrc/mem128[64-95] + xmmsrc/mem128[96-127]
xmmdest = temp

Сложение с помощью 128-битной инструкции vphaddd

xmmdest[0-31] = xmmsrc1[0-31] + xmmsrc1[32-63]
xmmdest[32-63] = xmmsrc1[64-95] + xmmsrc1[96-127]
xmmdest[64-95] = xmmsrc2/mem128[0-31] + xmmsrc2/mem128[32-63]
xmmdest[96-127] = xmmsrc2/mem128[64-95] + xmmsrc2/mem128[96-127]
(ymmdest[128-255] = 0)

Сложение с помощью 256-битной инструкции vphaddd

ymmdest[0-31] = ymmsrc1[32-63] + ymmsrc1[0-31]
ymmdest[32-63] = ymmsrc1[96-127] + ymmsrc1[64-95]
ymmdest[64-95] = ymmsrc2/mem128[32-63] + ymmsrc2/mem128[0-31]
ymmdest[96-127] = ymmsrc2/mem128[96-127] + ymmsrc2/mem128[64-95]
ymmdest[128-159] = ymmsrc1[160-191] + ymmsrc1[128-159]
ymmdest[160-191] = ymmsrc1[224-255] + ymmsrc1[192-223]
ymmdest[192-223] = ymmsrc2/mem128[160-191] + ymmsrc2/mem128[128-159]
ymmdest[224-255] = ymmsrc2/mem128[224-255] + ymmsrc2/mem128[192-223]

Если при горизонтальном сложеним с помощью инструкций (v)phaddw и (v)phaddd происходит переполнение, то оно просто игнорируется. При горизонтальном сложении с насыщением с помощью phaddsw любое (положительное) переполнение приводит к значению 0x7FFF, независимо от фактического результата. Аналогично, любое отрицательное значение потери значимости приводит к значению 0x8000.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850