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

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

Расширения SSE/AVX предоставляют ряд инструкций для умножения чисел:

  • pmullw: перемножает слова (word) из 8 дорожек, младшее слово результата помещается в итоговую дорожку

  • vpmullw: перемножает слова из 8 дорожек (128-битная версия) или из 16 дорожек (256-битная версия), младшее слово результата помещается в итоговую дорожку

  • pmulhuw: перемножает беззнаковые слова из 8 дорожек, старшее слово результата помещается в итоговую дорожку

  • vpmulhuw: перемножает слова из 8 дорожек (128-битная версия) или из 16 дорожек (256-битная версия), старшее слово результата помещается в итоговую дорожку

  • pmulhw: перемножает слова со знаком из 8 дорожек, старшее слово результата помещается в итоговую дорожку

  • vpmulhw: перемножает слова из 8 дорожек (128-битная версия) или из 16 дорожек (256-битная версия), старшее слово результата помещается в итоговую дорожку

  • pmulld: перемножает двойные слова (dword) из 4 дорожек, младшее двойное слово результата помещается в итоговую дорожку

    Схематично это выглядит так:

    Temp0[63:0] := SRC1[31:0] * SRC2[31:0]
    Temp1[63:0] := SRC1[63:32] * SRC2[63:32]
    Temp2[63:0] := SRC1[95:64] * SRC2[95:64]
    Temp3[63:0] := SRC1[127:96] * SRC2[127:96]
    DEST[31:0] := Temp0[31:0]
    DEST[63:32] := Temp1[31:0]
    DEST[95:64] := Temp2[31:0]
    DEST[127:96] := Temp3[31:0]
    DEST[MAXVL-1:128] := 0
    
  • vpmulld: перемножает двойные слова из 4 дорожек (128-битная версия) или из 8 дорожек (256-битная версия), младшее двойное слово результата помещается в итоговую дорожку

    Схематично это выглядит так (для 128-битной версии):

    Temp0[63:0] := SRC1[31:0] * SRC2[31:0]
    Temp1[63:0] := SRC1[63:32] * SRC2[63:32]
    Temp2[63:0] := SRC1[95:64] * SRC2[95:64]
    Temp3[63:0] := SRC1[127:96] * SRC2[127:96]
    DEST[31:0] := Temp0[31:0]
    DEST[63:32] := Temp1[31:0]
    DEST[95:64] := Temp2[31:0]
    DEST[127:96] := Temp3[31:0]
    DEST[MAXVL-1:128] := 0
    

    Для 256-разрядной версии

    Temp0[63:0] := SRC1[31:0] * SRC2[31:0]
    Temp1[63:0] := SRC1[63:32] * SRC2[63:32]
    Temp2[63:0] := SRC1[95:64] * SRC2[95:64]
    Temp3[63:0] := SRC1[127:96] * SRC2[127:96]
    Temp4[63:0] := SRC1[159:128] * SRC2[159:128]
    Temp5[63:0] := SRC1[191:160] * SRC2[191:160]
    Temp6[63:0] := SRC1[223:192] * SRC2[223:192]
    Temp7[63:0] := SRC1[255:224] * SRC2[255:224]
    DEST[31:0] := Temp0[31:0]
    DEST[63:32] := Temp1[31:0]
    DEST[95:64] := Temp2[31:0]
    DEST[127:96] := Temp3[31:0]
    DEST[159:128] := Temp4[31:0]
    DEST[191:160] := Temp5[31:0]
    DEST[223:192] := Temp6[31:0]
    DEST[255:224] := Temp7[31:0]
    DEST[MAXVL-1:256] := 0
    
  • vpmullq: перемножает четверные слова (qword) из 2 дорожек (128-битная версия) или из 4 дорожек (256-битная версия), младшее четверное слово результата помещается в итоговую дорожку

Проблема умножения дорожек связана с тем, что при умножении двух n-битных чисел результат надо помещать в дорожку, которая занимает n бит, хотя умножение n × n может дать результат 2×n бит. Таким образом, операция умножения по дорожкам создает проблемы, поскольку теряется переполнение. Подобные инструкции предполагают, что результат может занять 2*n бит, однако в итоговую дорожку регистра помещается только младшие или старшие n бит результата.

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

pmullw xmmdest, xmmsrc/mem128
vpmullw xmmdest, xmmsrc, xmm/mem128
vpmullw ymmdest, ymmsrc, ymm/mem256

pmulhuw xmmdest, xmmsrc/mem128
vpmulhuw xmmdest, xmmsrc, xmm/mem128
vpmulhuw ymmdest, ymmsrc, ymm/mem256

pmulhw xmmdest, xmmsrc/mem128
vpmulhw xmmdest, xmmsrc, xmm/mem128
vpmulhw ymmdest, ymmsrc, ymm/mem256

pmulld xmmdest, xmmsrc/mem128
vpmulld xmmdest, xmmsrc, xmm/mem128
vpmulld ymmdest, ymmsrc, ymm/mem256

vpmullq xmmdest, xmmsrc, xmm/mem128
vpmullq ymmdest, ymmsrc, ymm/mem256

Пример перемножения на Linux:

global main

extern printf

section .data
nums0 dd 5, 6, 7, 8
nums1 dd 4, 5, 6, 7
format_str db "%d, %d, %d, %d", 10, 0

section .text
main: 
    sub rsp, 8

    movaps xmm0, [nums0]
    movaps xmm1, [nums1]
    vpmulld xmm0, xmm0, xmm1       ; XMM0 = XMM0 * XMM1 
    ; XMM0 = 20, 30, 42, 56

    ; выводим данные на консоль
    movd esi, xmm0
    psrldq xmm0, 4
    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

Здесь перемножаются 32-битные дорожки регистров %xmm0 и %xmm1. Консольный вывод программы:

root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm# gcc -static hello.o -o hello root@Eugene:~/asm# ./hello 20, 30, 42, 56 root@Eugene:~/asm#

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

global main

extern printf

section .data
nums0 dd 5, 6, 7, 8
nums1 dd 4, 5, 6, 7
format_str db "%d, %d, %d, %d", 10, 0

section .text
main: 
    sub rsp, 40
    movaps xmm0, [rel nums0]
    movaps xmm1, [rel nums1]
    vpmulld xmm0, xmm0, xmm1       ; XMM0 = XMM0 * XMM1 
    ; XMM0 = 20, 30, 42, 56
    
    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

Ряд инструкций позволяют обойти ограничения на разрядность результата

  • pmuldq: умножает двойные слова (dword) со знаком в 32-битных дорожках 0 и 2 и сохраняет 64-битные результаты в 64-битных дорожках 0 и 1 (они же 32-битные дорожки 0, 1, 2 и 3). Старшие 128 бит перекрывающего регистра YMM остаются без изменений

  • pmuludq: умножает беззнаковые двойные слова в 32-битных дорожках 0 и 2 и сохраняет 64-битные результаты в 64-битных дорожках 0 и 1 (они же 32-битные дорожки 0, 1, 2 и 3). Старшие 128 бит перекрывающего регистра YMM остаются без изменений

  • vpmuldq: 256-битные варианты умножает двойные слова со знаком в 32-битных дорожках 0, 2, 4, 6 и сохраняет 64-битные результаты в 64-битных дорожках 0, 1, 2 и 3 . 128-битная версия аналогична pmuldq за тем исключением, что старшие 128 бит перекрывающего регистра YMM заполняются нулями

  • vpmuludq: 256-битные варианты умножает беззнаковые двойные слова в 32-битных дорожках 0, 2, 4, 6 и сохраняет 64-битные результаты в 64-битных дорожках 0, 1, 2 и 3. 128-битная версия аналогична pmuludq за тем исключением, что старшие 128 бит перекрывающего регистра YMM заполняются нулями

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

pmuldq xmmdest, xmmsrc/mem128
vpmuldq xmmdest, xmmsrc1, xmm/mem128
vpmuldq ymmdest, ymmsrc1, ymm/mem256

pmuludq xmmdest, xmmsrc/mem128
vpmuludq xmmdest, xmmsrc1, xmm/mem128
vpmuludq ymmdest, ymmsrc1, ymm/mem256

И еще пара инструкций - pclmulqdq и vpclmulqdq умножают четверные слова (qword) и сохраняют 128-битныq результат. pclmulqdq оставляет старшие 128 бит перекрывающего регистра YMM без изменений, а vpclmulqdq заполняет их нулями.

pclmulqdq xmmdest, xmmsrc/mem128, imm8
vpclmulqdq xmmdest, xmmsrc1, xmmsrc2/mem128, imm8

Операнд imm8 указывает, какие четверные слова использовать в качестве исходных операндов. На примере pclmulqdq:

imm8

Результат

0x00

XMMdest = xmmdest[0-63] * XMM/mem128[0-63]

0x01

XMMdest = xmmdest[64-127] * XMM/mem128[0-63]

0x10

XMMdest = xmmdest[0-63] * XMM/mem128[64-127]

0x11

XMMdest = xmmdest[64-127] * XMM/mem128[64-127]

vpclmulqdq работает аналогично, только числа берутся из второго и третьего операнда.

Пример умножения 64-разрядных чисел на Linux:

global main

extern printf

section .data
nums0 dq 5, 6
nums1 dq 3, 4
format_str db "%d", 10, 0

section .text
main: 
    sub rsp, 8

    movaps xmm0, [nums0]
    movaps xmm1, [nums1]
    vpclmulqdq xmm0, xmm0, xmm1, 0x00
    ; XMM0[0:63] = 15

    ; выводим данные на консоль
    movq rsi, xmm0
    mov rdi, format_str
    call printf

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