Расширения SSE/AVX предоставляют следующие поразрядные логические операции, которые выполняются над векторами чисел:
andpd
: логическое умножение dest = dest and source
(128-разрядные операнды)
vandpd
: логическое умножение dest = source1 and source2
(128- или 256-разрядные операнды)
andnpd
: логическое умножение с отрицанием (NAND) dest = dest and ~source
(128-разрядные операнды)
vandnpd
: логическое умножение с отрицанием (NAND) dest = source1 and ~source2
(128- или 256-разрядные операнды)
orpd
: логическое сложение dest = dest | source
(128-разрядные операнды)
vorpd
: логическое сложение dest = source1 | source2
(128- или 256-разрядные операнды)
xorpd
: операция XOR dest = dest ^ source
(128-разрядные операнды)
vxorpd
: операция XOR dest = source1 ^ source2
(128- или 256-разрядные операнды)
Синтаксис инструкций:
andpd xmmdest, xmmsrc/mem128 vandpd xmmdest, xmmsrc1, xmmsrc2/mem128 vandpd ymmdest, ymmsrc1, ymmsrc2/mem256 andnpd xmmdest, xmmsrc/mem128 vandnpd xmmdest, xmmsrc1, xmmsrc2/mem128 vandnpd ymmdest, ymmsrc1, ymmsrc2/mem256 orpd xmmdest, xmmsrc/mem128 vorpd xmmdest, xmmsrc1, xmmsrc2/mem128 vorpd ymmdest, ymmsrc1, ymmsrc2/mem256 xorpd xmmdest, xmmsrc/mem128 vxorpd xmmdest, xmmsrc1, xmmsrc2/mem128 vxorpd ymmdest, ymmsrc1, ymmsrc2/mem256
Инструкции SSE (без префикса v) оставляют старшие биты в целевом регистре YMM без изменений. Инструкции AVX (с префиксом v), которые имеют 128-битные операнды, заполняют старшие 128 бит регистра YMM нулями. Если первый операнд является переменной, он должен быть выровнен по соответствующей границе (например, 16 байтов для значений mem128 и 32 байта для значений mem256). Невыполнение этого требования приведет к ошибке выравнивания памяти во время выполнения.
Пример применения:
global _start section .data nums0 dd 0, 1, 0, 1 nums1 dd 0, 1, 1, 0 section .text _start: movaps xmm0, [nums0] ; вектор num0 в регистр xmm0 movaps xmm1, [nums1] ; вектор num1 в регистр xmm1 andpd xmm0, xmm1 ; XMM0 = 0, 1, 0, 0 mov rax, 60 syscall
Здесь в регистры XMM0 и XMM1 загружаются соответственно векторы nums0 и nums1. Далее к элементам этих векторов применяется логическая операции AND, которая возвращает 1, если только обы разряда двух операндов равны 1. То есть мы получим следующие вычисления:
0, 1, 0, 1 * 0, 1, 1, 0 = 0, 1, 0, 0
В итоге в регистре XMM0 будет содержаться вектор 0, 1, 0, 0
Мы можем это проверить, выведя значения на консоль (пример для Linux):
global main extern printf section .data nums0 dd 0, 1, 0, 1 nums1 dd 0, 1, 1, 0 format_str db "%d, %d, %d, %d", 10, 0 section .text main: sub rsp, 8 movaps xmm0, [nums0] ; вектор num0 в регистр xmm0 movaps xmm1, [nums1] ; вектор num1 в регистр xmm1 andpd xmm0, xmm1 ; XMM0 = 0, 1, 0, 0 movd esi, xmm0 ; помещаем первое число в rsi psrldq xmm0, 4 ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd edx, xmm0 ; помещаем первое число в rdx psrldq xmm0, 4 ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd ecx, xmm0 ; помещаем первое число в rcx psrldq xmm0, 4 ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd r8d, xmm0 ; помещаем первое число в r8 mov rdi, format_str call printf add rsp, 8 ret
Для вывода значений на консоль здесь используется библиотечная функция языка Си - printf
. Соответственно для взаимодействия с Си программа определена как функция main.
После выполнения операции AND каждое отдельное число вектора в %xmm0 помещаем в регистры %rsi, %edx, %ecx, %r8d для передачи чисел в функцию printf.
Пример компиляции и работы программы:
root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm# gcc -static hello.o -o hello root@Eugene:~/asm# ./hello 0, 1, 0, 0 root@Eugene:~/asm#
Аналогичная программа на Windows:
global main extern printf section .data nums0 dd 0, 1, 0, 1 nums1 dd 0, 1, 1, 0 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 andpd xmm0, xmm1 ; XMM0 = 0, 1, 0, 0 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
Другой пример - манипуляция со строками. Например, переведем строку в нижний регистр в программе на Linux:
global main extern printf section .data message db "HeLLo",0 maxlen equ ($-message-1) ; вычитаем также концевой нулевой байт align 16 ; для заполнения оставшегося пространства строки message нулями mask1: times maxlen db 0x20 times (16-maxlen) db 0 mask2: times maxlen db 0xDF times (16-maxlen) db 0 format_str db "%s", 10, 0 section .text main: sub rsp, 8 movdqa xmm0, [message] ; копируем строку из message в регистр xmm0 orpd xmm0, [mask1] ; переводим в нижний регистр ; andpd xmm0, mask2 ; переводим в верхний регистр movdqa [message], xmm0 ; копируем строку из регистр xmm0 в message mov rdi, format_str mov rsi, message call printf add rsp, 8 ret
Тестовая строка в нашем случае представляет переменную str и имеет значение "HeLLo". Для простоты примера предположим, что строка не больше 16 байт, с учетом концевого нулевого байта. Буквы в нижнем регистре отличаются от букв в верхнем регистре в таблице ASCII установленным битом 5 (нумерация битов с нуля). Например, буква "A" имеет двоичный код 01000001, а буква "a" - 01100001. То есть, чтобы перейти от буквы в верхнем регистре к букве в нижнем регистре, нам надо устровить бит 5. И для этого определяем маску:
mask1: times maxlen db 0x20 times (16-maxlen) db 0
То есть здесь определяем набор из maxlen байтов, каждый из которых равен 0x20 или 0b00100000 в двоичной системе, то есть каждое из чисел маски имеет установленный 5-й битю Остальное пространство маски заполнено нулями.
Строку str загружаем в регистр xmm0 и с помощью инструкции
orpd xmm0, [mask1]
Для каждого символа (байта) в строке устанавливаем 5-й бит
Затем сохраняем строку обратно в переменную str и выводим на консоль. Конечно, для полноценной работы со строками это немного наивный пример, поскольку мы не учитываем неалфавиитные символы и строки, которые больше 16 байт. Тем не менее данный пример показывает, что благодаря расширениям SSE/AVX мы можем проще проворачивать операции со строками. Пример компиляции и работы программы:
root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm# gcc -static hello.o -o hello root@Eugene:~/asm# ./hello hello root@Eugene:~/asm#
Аналогично для перевода буквы в верхний регистр нам надо обнулить 5-й бит. Для этого применяем маску
mask2: times maxlen db 0xDF times (16-maxlen) db 0
Число 0xDF
(0b11011111 в двоичной системе) с помощью операции AND позволяет обнулить 5-й бит символа (байта):
andpd xmm0, [mask2]