Копирование данных с помощью инструкций SIMD

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

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

Копирование скалярных значений

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

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
vmovd reg32/mem32, xmm
vmovq xmm, reg64/mem64
vmovq reg64/mem64, xmm

Поскольку в этих операциях участвуют 32- и 64-разрядные операнды, то для переменных необязательно выравнивание в памяти по 16/32/64 битам (хотя при наличии подобного выравнивания инструкции могут выполняться быстрее).

Пример копирования:

.data
    num dword 22
.code
main proc       
    mov eax, 7
    ; в XMM
    movq xmm0, rax  ; в XMM0 число 7
    movd xmm1, num  ; в XMM1 число 22

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

    add eax, num    ; eax = 22 + 7 = 29
    ret
main endp
end

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

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

Отдельно есть инструкции, которые выполняют копирование выровненных данных

  • movaps: копирование числа с плавающей точкой одинарной точности. Допускает переменные типов real4, dword и oword

  • movapd: копирование числа с плавающей точкой одинарной точности. Допускает переменные типов real8, qword и oword

  • movdqa: копирование восьмеричного слова (oword). Допускает только переменные типа oword

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

  • vmovaps: копирование числа с плавающей точкой одинарной точности. Допускает переменные типов real4, dword и (при использовании регистров YMM) ymmword ptr

  • vmovapd: копирование числа с плавающей точкой одинарной точности. Допускает переменные типов real8, qword и (при использовании регистров YMM) ymmword ptr

  • vmovdqa: копирование восьмеричного слова (oword). При использовании регистров YMM допускает только переменные типа ymmword ptr

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

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

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

Операнд mem128 должен быть выровнен по 16-байтовой границе и в зависимости от инструкции должен представлять:

  • для инструкции (v)movaps - вектор из четырех 32-разрядных чисел

  • для инструкции (v)movapd - вектор из двух 64-разрядных чисел

  • для инструкции (v)movdqa - 16-байтовое значение (16 байтов, 8 word, 4 dword или 2 qword)

Операнд mem256 должен быть выровнен по 32-байтовой границе и в зависимости от инструкции должен представлять:

  • для инструкции vmovaps - вектор из восьми 32-разрядных чисел

  • для инструкции vmovapd - вектор из четырех 64-разрядных чисел

  • для инструкции vmovdqa - 32-байтовое значение (32 байта, 16 word, 8 dword или 4 qword)

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

.data
    qnums qword 121, 122
    qnums_copy qword 2 dup(?)
.code
main proc       
    ; в XMM
    movapd xmm0, qnums

    ; из XMM
    movapd qnums_copy, xmm0
    
    mov rax, qnums_copy     ; rax = 121
    ret
main endp
end

В данном случае определены два вектора, они состоят из 2-х чисел qword.

С помощью инструкции movapd вектор qnums загружается в регистр XMM0. Затем эти данные выгружаются обратно в переменную qnums_copy.

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

.data
qnums qword 121, 122, 123, 124

    ................................
movapd xmm0, qnums

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

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

includelib kernel32.lib   ; подключаем библиотеку kernel32.lib

; подключаем функции WriteFile и GetStdHandle
extrn WriteFile: PROC
extrn GetStdHandle: PROC

len equ 16
.data
  source byte "Hello METANIT",10
  align 16
  dest byte len dup(?)
.code


main proc
  
  movdqa xmm0, oword ptr source    ; копируем из source в %ymm0
  movdqa oword ptr dest, xmm0      ; копируем из %ymm0 в dest

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

  ret
main endp
end

В данном случае копируем строку source в регистр xmm0, а затем из регистра xmm0 - в переменную dest. Затем выводим строку на консоль с помощью функции WriteLine.

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

.data
    temp1 byte 1
    qnums qword 121, 122
    temp2 byte 31
    qnums_copy qword 2 dup(?)
.code
main proc       
    ; в XMM
    movapd xmm0, qnums 

    ; из XMM
    movapd qnums_copy, xmm0
    
    mov rax, qnums_copy ; для теста получим в RAX первый элемент вектора qnums_copy
    ret
main endp
end

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

.data
    temp1 byte 1
    align 16
    qnums qword 121, 122
    temp2 byte 31
    align 16
    qnums_copy qword 2 dup(?)

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

.data
    qnums qword 121, 122
    qnums_copy qword 2 dup(?)

    temp1 byte 1
    temp2 byte 31

Сегменты с выравниванием

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

.data
    temps qword 1, 2, 3
align 32    ; Ошибка: 32 - некорректное значение
    nums qword 11, 22, 33, 44
    nums_copy  qword 4 dup(?)
.code
main proc 
    vmovapd ymm0, nums
    vmovapd nums_copy, ymm0

    mov rax, nums_copy
    ret
main endp
end

Здесь мы столкнемся с ошибкой, так как 32 - некорректное значение для установки выравнивания. С аналогичной проблемой мы столкнемся, если захотим выравнить данные по 64 байтам, например, для последущего копирования в 516-разрядный регистр ZMM.

Чтобы решить проблему, нам надо определить сегмент с установкой выравнивания. Сегмент определяется следующим образом:

имя_сегмента segment readonly alignment
    инструкции
имя_сегмента ends

Сначала идет имя сегмента, после которого указывается оператор segment. После слова segment указывается параметр выравнивания, который может принимать значения: byte (выравнивае по 1 байту), word (по 2 байтам), dword (по 4 байтам), para (выравнивание по параграфу - 16 байтам), page (по странице - 256 байтам), align(n) (выравнивание по n байт). Если сегмент надо сделать доступным только для чтения (например, если его переменные не планируется изменять), то после слова segment указывается слово readonly.

После выравнивания размещаются инструкции или определения данных. Завершается сегмент также именем сегмента с оператором ends.

Например, перепишем предыдущтй пример, используя сегменты:

dataSeg segment align(32)
    temps qword 1, 2, 3
    align 32
    nums qword 11, 22, 33, 44 
    nums_copy qword 4 dup(?)
dataSeg ends
.code
main proc 
    vmovapd ymm0, nums
    vmovapd nums_copy, ymm0

    mov rax, nums_copy
    ret
main endp
end

Здесь сегмент называется dataSeg. Он вырованен по 32 байтам (align(32)), что гарантирует, что первая переменная будет выровнена по 32 байтам. Кроме того, внутри сегмента мы можем использовать директиву align 32, чтобы также выровнять переменную nums по 32 байтам. Но опять же в данном случае, если порядок данных в сегменте не важен, то мы могли бы перегруппировать данные, чтобы избежать ненужного выравнивания:

dataSeg segment align(32)
    nums dword 12, 22, 33, 44, 55, 66, 77, 88
    nums_copy  dword 8 dup(?)
    temps dword 1, 2, 3, 4
dataSeg ends

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

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

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

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

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

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

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