Архитектура x86-64 предоставляет множество инструкций для копирования данных, которые копируют данные между регистрами (SSE/AVX), получают данные из регистров общего назначения или переменных или сохраняют данные обратно в переменные и другие регистры.
Расширения SSE предоставляют инструкции movd (для копирования 32-разрядных чисел) и movq (для копирования 64-разрядных чисел):
movd %reg32/mem32, %xmm movq %reg64/mem64, %xmm
Инструкция movd копирует значение из 32-разрядного регистра общего назначения или переменной в младшие 32 бита регистра XMM. А инструкция movq копирует значение из 64-разрядного регистра общего назначения или переменной в младшие 64 бита регистра XMM.
Также эти инструкции поддерживают обратную операцию - сохранение данных из регистров XMM в 32- или 64-разрядный регистр общего назначения или переменную:
movd %xmm, %reg32/mem32 movq %xmm, %reg64/mem64
Инструкция movq также позволяет копировать данные из младших 64 бит одного регистра XMM в другой регистр XMM:
movq xmm, xmm
Расширение AVX поддержиет аналогичные инструкции, только их название предваряется символом v:
vmovd %reg32/mem32, %xmm vmovq %reg64/mem64, %xmm vmovd %xmm, %reg32/mem32 vmovq %xmm, %reg64/mem64
Данные инструкции применяются как для копирования целых чисел, так и чисел с плавающей точкой. Пример копирования:
.globl _start .data num: .long 22 .text _start: movq $7, %rax # в XMM movq %rax, %xmm0 # в XMM0 число 7 movd num, %xmm1 # в XMM1 число 22 # из XMM movq %xmm1, %rdi # RDI = 22 movd %xmm0, num # num = 7 addq num, %rdi # RDI = RDI + num = 22 + 7 = 29 movq $60, %rax syscall
Аналогично копируются числа с плавающей точкой
.globl _start .data num1: .double 2.13 num2: .float 3.14 .text _start: movq num1, %xmm0 # XMM0 = 2.13 movd num2, %xmm1 # XMM1 = 3.14 movq $60, %rax 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" (выровненный)
Подобные инструкции позволяют нам легко скопировать большой набор данных из одного набора в другой:
.globl _start .data source: .long 12, 13, 14, 15 dest: .fill 4, 4, 0 .text _start: movaps source, %xmm0 # копируем из source в %xmm0 movaps %xmm0, dest # копируем из %xmm0 в dest movl dest, %edi # RDI = 12 movq $60, %rax syscall
В данном случае определены два вектора - source и dest. Вектор source копируется в регистр %xmm0. Затем вектор чисел из регистра %xmm0 копируется в dest. Таким образом, с помощью двух инструкций мы скопировали набор из 4 32-разрядных чисел типа. При использовании расширений AVX2/AVX512 и соответственно регистров YMM/ZMM количество чисел увеличивается.
Как выше говорилось, для определенных инструкций мы можем передать определенное количество данных определенного типа. Так, в примере выше мы передаем инструкции movaps
вектор из 4 32-разрядных чисел. Что будет, если мы передадим вектор из большего количества чисел? Например:
.data source: .long 12, 13, 14, 15, 16, 17, 18 ............................ movaps source, %xmm0 # копируем из source в %xmm0
В этом случае в XMM0 будут загружены только 4 первых числа, что вообщем-то логично, так как 128-разрядный регистр XMM0 вмешает только четыре 32-разрядных числа.
Аналогично можно копировать и строки:
.globl _start .data source: .ascii "Hello METANIT.COM\n" .balign 16 dest: .space len len = 256 .text _start: vmovapd source, %ymm0 # копируем из source в %ymm0 vmovapd %ymm0, dest # копируем из %ymm0 в dest # выводим строку dest на консоль movq $1, %rdi movq $dest, %rsi movq $len, %rdx movq $1, %rax syscall movq $60, %rax syscall
Здесь копируем строку source через регистр 256-разрядный регистр %ymm0 в переменную dest.
Но стоит не забывать главную вещь - эти инструкции требуют выравнивания. Например, в примере выше выравнивание устанавливалось по умолчанию - обе переменных автоматически было выровнены
по 16 байтам (как того требует инструкции movaps
), поскольку перед первой переменной ничего нет, а она сама занимает 4 * 32 = 128 байт, то есть размер кратный 16. Соответственно вторая переменная также была выравнена по 16 байтам.
Но возьмем другой пример:
.globl _start .data temp1: .byte 1 source: .long 12, 13, 14, 15 # данные НЕ выровнены по 16 битам temp2: .byte 2 dest: .fill 4, 4, 0 # данные НЕ выровнены по 16 битам .text _start: movaps source, %xmm0 # копируем из source в %xmm0 movaps %xmm0, dest # копируем из %xmm0 в dest movl dest, %edi # RDI = 12 movq $60, %rax syscall
Перед вектором source идет однобайтная переменная, поэтому он не выровнен по 16 байт, соответственно также не выровнен и вектор dest. И здесь как раз можно использовать выравнивание:
.data temp1: .byte 1 .balign 16 # последующие данные выровнены по 16 битам source: .long 12, 13, 14, 15 temp2: .byte 2 .balign 16 # последующие данные выровнены по 16 битам dest: .fill 4, 4, 0
Из минусов - программа занимает больше места. Однако если порядок переменных не играет значения, то лучше переупорядочить, чтобы избежать избыточных выравниваний:
.data source: .long 12, 13, 14, 15 dest: .fill 4, 4, 0 temp1: .byte 1 temp2: .byte 2
Если нельзя гарантировать, что переменные-операнды выровнены по 16- или 32-байтовой адресной границе, то для копирования данных между регистрами XMM или YMM и памятью можно использовать ряд других инструкций:
(v)movups
: копирование невыровненных 4-байтных чисел
(v)movupd
: копирование невыровненных 8-байтных чисел
(v)movdqu
: копирование невыровненных 16-байтных чисел
Буква u является сокрашением от "unaligned" ("невыровненный"). Работают они аналогично:
.globl _start .data source: .quad 22, 23, 24, 25 dest: .fill 4, 8, 0 .text _start: movupd source, %xmm0 # копируем из source в %xmm0 movupd %xmm0, dest # копируем из %xmm0 в dest movq dest, %rdi # RDI = 22 movq $60, %rax syscall
Хотя считается, что эти инструкции работают в целом медленнее, чем их аналоги для выровненных данных.