Расширения SSE и AVX/AVX2

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

Архитектура x86-64 предоставляет множество инструкций для копирования данных, которые копируют данные между регистрами (SSE/AVX), получают данные из регистров общего назначения или переменных или сохраняют данные обратно в переменные и другие регистры.

Копирование одного значения

Расширения SSE предоставляют инструкции movd (для копирования 32-разрядных чисел) и movq (для копирования 64-разрядных чисел):

movd xmm, reg32/mem32
movq xmm, reg64/mem64

Инструкция movd копирует значение из 32-разрядного регистра общего назначения или переменной в младшие 32 бита регистра XMM. А инструкция movq копирует значение из 64-разрядного регистра общего назначения или переменной в младшие 64 бита регистра XMM.

Также эти инструкции поддерживают обратную операцию - сохранение данных из регистров XMM в 32- или 64-разрядный регистр общего назначения или переменную:

movd reg32/mem32, xmm
movq reg64/mem64, xmm

Инструкция movq также позволяет копировать данные из младших 64 бит одного регистра XMM в другой регистр XMM:

movq xmm, xmm

Расширение AVX поддержиет аналогичные инструкции, только их название предваряется символом v:

vmovd xmm, reg32/mem32
vmovq xmm, reg64/mem64
vmovd reg32/mem32, xmm
vmovq reg64/mem64, xmm

Данные инструкции применяются как для копирования целых чисел, так и чисел с плавающей точкой. Пример копирования на Linux:

global _start

section .data
num dw 22

section .text
_start:
    mov rax, 7 
    ; в XMM
    movq xmm0, rax  ; в XMM0 число 7
    movd xmm1, [num]  ; в XMM1 число 22

    ; из XMM
    movq rdi, xmm1  ; RDI = 22
    movd [num], xmm0 ; num = 7

    ; для проверки устанавливаем код возврата
    add rdi, [num]  ; RDI = RDI + num = 22 + 7 = 29

    mov rax, 60
    syscall

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

global _start

section .data
num dw 22

section .text
_start:
    mov rax, 7 
    ; в XMM
    movq xmm0, rax  ; в XMM0 число 7
    movd xmm1, [rel num]  ; в XMM1 число 22

    ; из XMM
    movq rax, xmm1  ; RAX = 22
    movd [rel num], xmm0 ; num = 7

    ; для проверки устанавливаем код возврата
    add rax, [rel num]  ; RAX = RAX + num = 22 + 7 = 29
    ret

Подобным образом копируются и числа с плавающей точкой. Пример на Linux:

global _start

section .data
num0 dq 2.13
num1 dw 3.14

section .text
_start:
    movq xmm0, [num0] ; XMM0 = 2.13
    movd xmm1, [num1] ; XMM1 = 3.14

    mov rax, 60
    syscall

Копирование чисел с плавающей точкой

Для копирования отдельных чисел с плавающей точкой также есть пара инструкций:

  • movss: копирует одно 32-разрядное число с плавающей точкой

  • movsd: копирует одно 64-разрядное число с плавающей точкой

Их общий синтаксис:

movss xmmn, mem32
movss mem32, xmmn
movss xmmsrc, xmmdest
movsd xmmn, mem64
movsd mem64, xmmn
movsd xmmsrc, xmmdest

Можно копировать как в регистр xmm из переменной, либо в переменную из регистра xmm.

Для максимальной производительности переменные, используемые в инструкции movss, должны располагаться выравненны в памяти по двойному слову (4 бита), а операнды инструкции movsd — по адресу памяти, выровненному по четверному слову.

Копирование вектора данных

Для копирования вектора или набора данных предусмотрен ряд инструкций, которые могут копировать либо выровненные, либо не выровненные данные.

Копирование выровненных данных

Для копирования выровненных данных расширения SSE предоставляют следующий набор инструкций

  • movaps: копирование набора 32-разрядных чисел

  • movapd: копирование набора 64-разрядных чисел

  • movdqa: копирование восьмеричного слова

Эти инструкции копируют 16 байтов данных между переменными и регистрами XMM или между двумя регистрами XMM. Расширения AVX представляют аналогичные инструкции с префиксом v, которые копируют 16 или 32 байта между переменными и регистрами XMM или YMM или между двумя регистрами XMM или YMM (при копировании в регистры XMM обнуляются старшие биты соответствующего регистра YMM):

  • vmovaps: копирование набора 32-разрядных чисел

  • vmovapd: копирование набора 64-разрядных чисел

  • vmovdqa: копирование восьмеричного слова

Инструкции принимают следующие формы:

movaps mem128, xmm
vmovaps mem128, xmm
vmovaps mem256, ymm
movaps xmm, mem128
vmovaps xmm, mem128
vmovaps ymm, mem256
movaps xmm, xmm
vmovaps xmm, xmm
vmovaps ymm, ymm
movapd mem128, xmm
vmovapd mem128, xmm
vmovapd mem256, ymm
movapd xmm, mem128
vmovapd xmm, mem128
vmovapd ymm, mem256
movapd xmm, xmm
vmovapd xmm, xmm
vmovapd ymm, ymm
movdqa mem128, xmm
vmovdqa mem128, xmm
vmovdqa mem256, ymm
movdqa xmm, mem128
vmovdqa xmm, mem128
vmovdqa ymm, mem256
movdqa xmm, xmm
vmovdqa xmm, xmm
vmovdqa ymm, ymm

При копировании переменные должны быть выровнены по 16-байтовой или 32-байтовой границе (соответственно), иначе процессор сгенерирует ошибку невыровненного доступа. Собственно буква a в названии инструкций как раз представляет сокращение от "aligned" (выровненный)

Подобные инструкции позволяют нам легко скопировать большой набор данных из одного набора в другой:

global _start

section .data
align 16      ; гарантируем выравнивание
source dd 12, 13, 14, 15    ; откуда копируем

section .bss
align 16       ; гарантируем выравнивание
dest resd 4    ; куда копируем

section .text
_start:
    movaps xmm0, [source]    ; копируем из source в xmm0
    movaps [dest], xmm0      ; копируем из xmm0 в dest
    mov edi, [dest]        ; RDI = 12

    mov rax, 60
    syscall

В данном случае определены два вектора - source и dest. Стоит отметить, что оба вектора выровнены по 16 байтам. Вектор source копируется в регистр xmm0. Затем вектор чисел из регистра xmm0 копируется в dest. Таким образом, с помощью двух инструкций мы скопировали набор из 4 32-разрядных чисел типа. При использовании расширений AVX2/AVX512 и соответственно регистров YMM/ZMM количество чисел увеличивается.

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

global _start

section .data
align 16      ; гарантируем выравнивание
source dd 12, 13, 14, 15    ; откуда копируем

section .bss
align 16       ; гарантируем выравнивание
dest resd 1    ; куда копируем

section .text
_start:
    movaps xmm0, [rel source]    ; копируем из source в xmm0
    movaps [rel dest], xmm0      ; копируем из xmm0 в dest
    mov eax, [rel dest]       ; RAX = 12

    ret

Как выше говорилось, для определенных инструкций мы можем передать определенное количество данных определенного типа. Так, в примере выше мы передаем инструкции movaps вектор из 4 32-разрядных чисел. Что будет, если мы передадим вектор из большего количества чисел? Например:

section .data
align 16      ; гарантируем выравнивание
source dd 12, 13, 14, 15, 16, 17, 18 
............................
movaps xmm0, [source]    ; копируем из source в xmm0

В этом случае в XMM0 будут загружены только 4 первых числа, что вообщем-то логично, так как 128-разрядный регистр XMM0 вмешает только четыре 32-разрядных числа.

Аналогично можно копировать и строки. Пример на Linux:

global _start

section .data
align 16
source db "Hello METANIT.COM", 10, 0
len equ $ - source

section .bss
align 16       ; гарантируем выравнивание
dest resb len    ; куда копируем

section .text
_start:
    vmovapd ymm0, [source]   ; копируем из source в ymm0
    vmovapd [dest], ymm0      ; копируем из ymm0 в dest

    ; выводим строку dest на консоль
    mov rdi, 1
    mov rsi, dest 
    mov rdx, len
    mov rax, 1
    syscall

    mov rax, 60
    syscall

Здесь копируем строку source через регистр 256-разрядный регистр ymm0 в переменную dest.

Аналогичная программа для Windows:

global _start

extern GetStdHandle
extern WriteFile

section .data
align 16
source db "Hello METANIT.COM", 10, 0
len equ $ - source

section .bss
align 16       ; гарантируем выравнивание
dest resb len    ; куда копируем

section .text
_start:
    sub  rsp, 40   ; Для параметров функций WriteFile и GetStdHandle резервируем 40 байт (5 параметров по 8 байт)
    
    vmovapd ymm0, [rel source]   ; копируем из source в ymm0
    vmovapd [rel dest], ymm0      ; копируем из ymm0 в dest

    ; выводим строку dest на консоль
    mov  rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT
    call GetStdHandle ; вызываем функцию GetStdHandle
    mov  rcx, rax     ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли
    mov  rdx, dest    ; Второй параметр WriteFile - загружаем указатель на строку в регистр RDX
    mov  r8d, len      ; Третий параметр WriteFile - длина строки для записи в регистре R8D 
    xor  r9, r9       ; Четвертый параметр WriteFile - адрес для получения записанных байтов
    mov  qword [rsp + 32], 0  ; Пятый параметр WriteFile
    call WriteFile ; вызываем функцию WriteFile
    add  rsp, 40

    ret

Но стоит не забывать главную вещь - эти инструкции требуют выравнивания. Например, в примере выше выравнивание устанавливалось по умолчанию - обе переменных автоматически было выровнены по 16 байтам (как того требует инструкции movaps), поскольку перед первой переменной ничего нет, а она сама занимает 4 * 32 = 128 байт, то есть размер кратный 16. Соответственно вторая переменная также была выравнена по 16 байтам. Но возьмем другой пример программы под Linux:

global _start

section .data
temp1 db 1
source dd 12, 13, 14, 15    ; данные НЕ выровнены по 16 битам

section .bss
dest resd 4     ; данные могут быть НЕ выровнены по 16 битам

section .text
_start:
    movaps xmm0, [source]    ; копируем из source в xmm0
    movaps [dest], xmm0      ; копируем из xmm0 в dest

    mov edi, [dest]         ; RDI = 12

    mov rax, 60
    syscall

Перед вектором source идет однобайтная переменная, поэтому он не выровнен по 16 байт, соответственно также не выровнен и вектор dest. И здесь как раз можно использовать выравнивание:

section .data
temp1 db 1
align 16      ; последующие данные выровнены по 16 битам
source dd 12, 13, 14, 15

Из минусов - программа занимает больше места. Однако если порядок переменных не играет значения, то лучше переупорядочить, чтобы избежать избыточных выравниваний:

section .data
source dd 12, 13, 14, 15    ; данные выровнены по 16 битам
dest resd 4    ; данные выровнены по 16 битам
temp1 db 1.3

Копирование без выравнивания

Если нельзя гарантировать, что переменные-операнды выровнены по 16- или 32-байтовой адресной границе, то для копирования данных между регистрами XMM или YMM и памятью можно использовать ряд других инструкций:

  • (v)movups: копирование невыровненных 4-байтных чисел

  • (v)movupd: копирование невыровненных 8-байтных чисел

  • (v)movdqu: копирование невыровненных 16-байтных чисел

Буква u является сокрашением от "unaligned" ("невыровненный"). Работают они аналогично:

global _start

section .data
source dq 22, 23, 24, 25

section .bss
dest resd 8 

section .text
_start:
    movupd xmm0, [source]    ; копируем из source в xmm0
    movupd [dest], xmm0      ; копируем из xmm0 в dest

    mov rdi, [dest]        ; RDI = 22

    mov rax, 60
    syscall

Хотя считается, что эти инструкции работают в целом медленнее, чем их аналоги для выровненных данных.

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