Логические операции над векторами

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

Расширения 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]
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850