Расширения 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 xmmsrc/mem128, xmmdest vandpd xmmsrc2/mem128, xmmsrc1, xmmdest vandpd ymmsrc2/mem256, ymmsrc1, ymmdest andnpd xmmsrc/mem128, xmmdest vandnpd xmmsrc2/mem128, xmmsrc1, xmmdest vandnpd ymmsrc2/mem256, ymmsrc1, ymmdest orpd xmmsrc/mem128, xmmdest vorpd xmmsrc2/mem128, xmmsrc1, xmmdest vorpd ymmsrc2/mem256, ymmsrc1, ymmdest xorpd xmmsrc/mem128, xmmdest vxorpd xmmsrc2/mem128, xmmsrc1, xmmdest vxorpd ymmsrc2/mem256, ymmsrc1, ymmdest
Инструкции SSE (без префикса v) оставляют старшие биты в целевом регистре YMM без изменений. Инструкции AVX (с префиксом v), которые имеют 128-битные операнды, заполняют старшие 128 бит регистра YMM нулями. Если первый операнд является переменной, он должен быть выровнен по соответствующей границе (например, 16 байтов для значений mem128 и 32 байта для значений mem256). Невыполнение этого требования приведет к ошибке выравнивания памяти во время выполнения.
Пример применения:
.globl _start .data nums0: .long 0, 1, 0, 1 nums1: .long 0, 1, 1, 0 .text _start: movaps nums0, %xmm0 # вектор num0 в регистр xmm0 movaps nums1, %xmm1 # вектор num1 в регистр xmm1 andpd %xmm1, %xmm0 # XMM0 = 0, 1, 0, 0 movq $60, %rax 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
Мы можем это проверить, выведя значения на консоль:
.globl main .data nums0: .long 0, 1, 0, 1 nums1: .long 0, 1, 1, 0 format_str: .asciz "%d, %d, %d, %d\n" .text main: subq $8, %rsp movaps nums0, %xmm0 # вектор num0 в регистр xmm0 movaps nums1, %xmm1 # вектор num1 в регистр xmm1 andpd %xmm1, %xmm0 # XMM0 = 0, 1, 0, 0 movd %xmm0, %esi # помещаем первое число в %rsi psrldq $4, %xmm0 # сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd %xmm0, %edx # помещаем первое число в %rdx psrldq $4, %xmm0 # сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd %xmm0, %ecx # помещаем первое число в %rcx psrldq $4, %xmm0 # сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd %xmm0, %r8d # помещаем первое число в %r8 movq $format_str, %rdi call printf addq $8, %rsp ret
Для вывода значений на консоль здесь используется библиотечная функция языка Си - printf
. Соответственно для взаимодействия с Си программа определена как функция main.
После выполнения операции AND каждое отдельное число вектора в %xmm0 помещаем в регистры %rsi, %edx, %ecx, %r8d для передачи чисел в функцию printf.
Пример компиляции и работы программы:
root@Eugene:~/asm# gcc -static hello.s -o hello root@Eugene:~/asm# ./hello 0, 1, 0, 0 root@Eugene:~/asm#
Другой пример - манипуляция со строками. Например, переведем строку в нижний регистр:
.globl main .data str: .asciz "HeLLo" maxlen = .-str - 1 # вычитаем также концевой нулевой байт .balign 16 # для заполнения оставшегося пространства строки str нулями mask1: .fill maxlen, 1, 0x20 .fill 16-maxlen, 1, 0 mask2: .fill maxlen, 1, 0xDF .fill 16-maxlen, 1, 0 format_str: .asciz "%s\n" .text main: subq $8, %rsp movdqa str, %xmm0 # копируем строку из str в регистр xmm0 orpd mask1, %xmm0 # переводим в нижний регистр # andpd mask2, %xmm0 # переводим в верхний регистр movdqa %xmm0, str # копируем строку из регистр xmm0 в str movq $format_str, %rdi movq $str, %rsi call printf addq $8, %rsp ret
Тестовая строка в нашем случае представляет переменную str и имеет значение "HeLLo". Для простоты примера предположим, что строка не больше 16 байт, с учетом концевого нулевого байта. Буквы в нижнем регистре отличаются от букв в верхнем регистре в таблице ASCII установленным битом 5 (нумерация битов с нуля). Например, буква "A" имеет двоичный код 01000001, а буква "a" - 01100001. То есть, чтобы перейти от буквы в верхнем регистре к букве в нижнем регистре, нам надо устровить бит 5. И для этого определяем маску:
mask1: .fill maxlen, 1, 0x20 .fill 16-maxlen, 1, 0
То есть здесь определяем набор из maxlen байтов, каждый из которых равен 0x20 или 0b00100000 в двоичной системе, то есть каждое из чисел маски имеет установленный 5-й битю Остальное пространство маски заполнено нулями.
Строку str загружаем в регистр xmm0 и с помощью инструкции
orpd mask1, %xmm0
Для каждого символа (байта) в строке устанавливаем 5-й бит
Затем сохраняем строку обратно в переменную str и выводим на консоль. Конечно, для полноценной работы со строками это немного наивный пример, поскольку мы не учитываем неалфавиитные символы и строки, которые больше 16 байт. Тем не менее данный пример показывает, что благодаря расширениям SSE/AVX мы можем проще проворачивать операции со строками. Пример компиляции и работы программы:
root@Eugene:~/asm# gcc -static hello.s -o hello root@Eugene:~/asm# ./hello hello root@Eugene:~/asm#
Аналогично для перевода буквы в верхний регистр нам надо обнулить 5-й бит. Для этого применяем маску
mask2: .fill maxlen, 1, 0xDF .fill 16-maxlen, 1, 0
Число 0xDF
(0b11011111 в двоичной системе) с помощью операции AND позволяет обнулить 5-й бит символа (байта):
andpd mask2, %xmm0