Операции сдвига SSE/AVX

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

Расширения SSE/AVX поддерживают инструкции логического и арифметического сдвига:

  • pslldq: сдвигает данные в регистре XMM влево на количество байтов, указанное в операнде imm8. В освободившиеся младшие байты помещаются нули.

    pslldq xmmdest, imm8
  • vpslldq: берет значение из второго операнда-регистра XMM или YMM, сдвигает это значение влево на количество байт, указанное в третьем операнде. Затем сохраняет результат в регистре из первого операнда.

    vpslldq xmmdest, xmmsrc, imm8
    vpslldq ymmdest, ymmsrc, imm8
    

    128-битный вариант инструкция также заполняет нулями биты со 128 по 255 регистра YMM.

  • psrldq: сдвигает данные в регистре XMM вправо на количество байтов, указанное в операнде imm8. В освободившиеся старшие байты помещаются нули.

    psrldq xmmdest, imm8
  • vpsrldq: сдвигает вправл значение из второго операнда-регистра XMM или YMM на количество байт, указанное в третьем операнде. Затем сохраняет результат в первом операнде.

    vpsrldq xmmdest, xmmsrc, imm8
    vpsrldq ymmdest, ymmsrc, imm8
    

Стоит отметить, что эти инструкции сдвигают по байтам (то есть как минимум можно сдвинуть 8 разрядов) весь вектор, который хранится в регистре.

Пример сдвига в программе на Linux:

global _start

section .data
nums dd 3, 5, 8, 11

section .text
_start:
    movaps xmm0, [nums]   ; вектор nums в регистр xmm0
    psrldq xmm0, 4        ; сдвигаем регистр xmm0 вправо на 4 байта
    movd edi, xmm0        ; edi = 5
    mov rax, 60
    syscall

Здесь в регистр xmm0 помещается вектор nums0, который состоит из 4 чисел dword. Соответственно каждое число занимает 4 байта. Инструкция psrldq xmm0, 4 сдвигает содерживое регистра xmm0 вправо на 4 байта:

11, 8, 5, 3 >> 4 =
0, 11, 8, 5

В итоге в XMM0 будет вектор 0, 11, 8, 5

Далее первое число в векторе из xmm0 (которым после сдвига является число 5) помещается в регистр edi.

Используя операцию сдвига, нам легко реализовать вывод содержимого регистра xmmN на консоль. Например, определим следующую программу для Linux:

global _start

section .data
nums dd 254, 5, 8, 11
hexstr128 db "0x123456789ABCDEFG123456789ABCDEFG", 10, 0
len equ $-hexstr128    ; размер строки
hexmap db 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70


section .text
_start:
    movaps xmm0, [nums]     ; вектор num в регистр xmm0
    call print_reg128
    mov rax, 60
    syscall

; функция печати регистра на консоль
; Параметры: XMM0 - выводимое значение 
print_reg128:
    mov rsi, hexmap      ; таблица преобразования
    mov rbx, hexstr128     ; начало строки
    add rbx, 33         ; переходим на первый байт числа в строке hexstr128
    ; в цикле проходим от RCX = 16 до RCX=1 с приращением -1
    mov rcx, 16  ; 16 байтов - 32 символа для печати
forloop128: 
    movq rax, xmm0
    and rax, 0xf     ; накладываем маску - на число в AL
    mov al, byte [rsi + rax]  ; находим символьное отображение числа в таблице hexmap 
    mov byte [rbx], al   ; сохраняем один символ ascii

    movq rax, xmm0
    shr rax, 4       ; берем второй полубайт из XMM0
    and rax, 0xf     ; накладываем маску - на число в AL
    mov al, byte [rsi + rax]  ; находим символьное отображение числа в таблице hexmap 
    mov byte [rbx-1], al   ; сохраняем один символ ascii

    sub rbx, 2   ; вычитаем из адреса в RBX 2 для перехода к следующему символу

    psrldq xmm0, 1  ; сдвиг влево в XMM0 для получения следующего байта

    sub rcx, 1     ; уменьшаем счетчик RCX на 1 - для печати следующего символа
    jne forloop128           ; переходим к метке forloop128, если RCX не равно 1
    ; собственно печать строки с помощью системного вызова write
    mov rax, 1           ; номер системной функци
    mov rdi, 1           ; дескриптор стандартного (консольного) вывода
    mov rsi, hexstr128     ; адрес строки
    mov rdx, len         ; размер строки
    syscall                 ; выполняем системный вызов
    ret

В данном случае для печати регистра предназначена функция print_reg128. В качестве параметра она принимает значение через регистр xmm0. Для преобразования между 16-ричнымии числами и их символьным представлением применяется таблица-набор hexmap, которая загружается в регистр rsi:

mov rsi, hexmap    ; таблица преобразования

Данные будут сохраняться в строку hexstr128, адрес которой загружается в регистр rbx:

mov rbx, hexstr128     ; начало строки
add rbx, 33         ; переходим на первый байт числа в строке hexstr128

Причем устанавливаем адрес в rbx на 33 символ, с котором начнем вставку в строку чисел из xmm0.

В регистр rcx помещаем количество байт 128-разрядного регистра, которые нам надо просмотреть - то есть все 16 байт:

mov rcx, 16  ; 16 байт
forloop128: 

И после метки forloop128 идет циклическая конструкция, в которой перебираем регистр xmm0 по 4 бита и сопоставляем каждые из этих 4 битов с определенной цифрой в hexmap. Строковое отображение цифры помещаем в строку hexstr128. Причем поскольку мы перемещаемся по байтам, то за один виток цикла обрабатываем 2 полубайта. Сначала сохраняем в регистр AL первый полубайт из XMM0:

movq rax, xmm0
and rax, 0xf     ; накладываем маску - на число в AL
mov al, byte [rsi + rax]  ; находим символьное отображение числа в таблице hexmap 
mov byte [rbx], al   ; сохраняем один символ ascii

В al помещаем первый полубайт (первую 16-ричную цифру), находим символьное отображение числа в таблице hexmap и сохраняем символ по адресу в rbx (то есть адрес строки hexstr128).

Обработка второго полубайта аналогична, только символ сохраняем по адресу [rbx-1] (то есть смещаемся на один байт назад):

movq rax, xmm0
shr rax, 4       ; берем второй полубайт из XMM0
and rax, 0xf     ; накладываем маску - на число в AL
mov al, byte [rsi + rax]  ; находим символьное отображение числа в таблице hexmap 
mov byte [rbx-1], al   ; сохраняем один символ ascii

Когда обработка обоих полубайт первого байта из XMM0 завершилась, сдвигаем xmm0 на 1 байт вправо:

psrldq xmm0, 1  ; сдвиг влево в XMM0 для получения следующего байта

После обработки всех байтов и соответственно завершения цикла выводим строку hexstr128 на консоль с помощью системной функции write. Консольный вывод программы:

root@Eugene:~/asm; nasm -f elf64 hello.asm -o hello.o
root@Eugene:~/asm; ld hello.o -o hello
root@Eugene:~/asm; ./hello
0x0000000B0000000800000005000000FE
root@Eugene:~/asm;

Таким образом, регистр XMM0, который содержит вектор из 32-разрядных чисел 254, 5, 8, 11, в 16-ричном виде соответствует значению 0x0000000B0000000800000005000000FE

Аналогичная программа на Windows:

global _start

extern WriteFile        ; подключем функцию WriteFile
extern GetStdHandle     ; подключем функцию GetStdHandle

section .data
nums dd 254, 5, 8, 11
hexstr128 db "0x123456789ABCDEFG123456789ABCDEFG", 10, 0
len equ $-hexstr128    ; размер строки
hexmap db 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70

section .text
_start:
    movaps xmm0, [rel nums]     ; вектор num в регистр xmm0
    call print_reg128
    ret

; функция печати регистра на консоль
; Параметры: XMM0 - выводимое значение 
print_reg128:
    sub  rsp, 48   ; Для параметров функций WriteFile и GetStdHandle резервируем 40 байт (5 параметров по 8 байт)
    mov rsi, hexmap      ; таблица преобразования
    mov rbx, hexstr128     ; начало строки
    add rbx, 33         ; переходим на первый байт числа в строке hexstr128
    ; в цикле проходим от RCX = 16 до RCX=1 с приращением -1
    mov rcx, 16  ; 16 байтов - 32 символа для печати
forloop128: 
    movq rax, xmm0
    and rax, 0xf     ; накладываем маску - на число в AL
    mov al, byte [rsi + rax]  ; находим символьное отображение числа в таблице hexmap 
    mov byte [rbx], al   ; сохраняем один символ ascii

    movq rax, xmm0
    shr rax, 4       ; берем второй полубайт из XMM0
    and rax, 0xf     ; накладываем маску - на число в AL
    mov al, byte [rsi + rax]  ; находим символьное отображение числа в таблице hexmap 
    mov byte [rbx-1], al   ; сохраняем один символ ascii

    sub rbx, 2   ; вычитаем из адреса в RBX 2 для перехода к следующему символу

    psrldq xmm0, 1  ; сдвиг влево в XMM0 для получения следующего байта

    sub rcx, 1     ; уменьшаем счетчик RCX на 1 - для печати следующего символа
    jne forloop128           ; переходим к метке forloop128, если RCX не равно 1

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

Вывод данных с помощью printf

Аналогичным образом можно было бы выводить данные с помощью библиотечной функции printf языка Си. Рассмотрим на примере программы для Linux:

global main

extern printf

section .data
nums dd 255, 5, 8, 11
format_str db "%d, %d, %d, %d", 10, 0

section .text
main: 
    sub rsp, 8
    movaps xmm0, [nums]     ; вектор num0 в регистр xmm0

    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

Также чтобы получить каждое отдельное число вектора в xmm0, применяем операцию сдвига вправо и помещаем крайнее правое число вектора в один из регистров для вывода функцией 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
255, 5, 8, 11
root@Eugene:~/asm;

Вывод чисел с плавающей точкой

Вывод чисел с плавающей точкой чуть сложнее. Также рассмотрим на примере программы для Linux:

global main

extern printf

section .data
nums dd 1.2, 2.3, 3.4, 4.5
format_str db "%.1f, %.1f, %.1f, %.1f", 10, 0

section .text
main: 
    sub rsp, 8
    movaps xmm5, [nums]     ; вектор num0 в регистр xmm5

    movss xmm0, xmm5        ; помещаем первое число в xmm0
    cvtss2sd xmm0, xmm0   ; Преобразуем из float в double
    psrldq xmm5, 4        ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5
    movss xmm1, xmm5        ; помещаем второе число в xmm1
    cvtss2sd xmm1, xmm1   ; Преобразуем из float в double
    psrldq xmm5, 4        ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5
    movss xmm2, xmm5        ; помещаем третье число в xmm2
    cvtss2sd xmm2, xmm2   ; Преобразуем из float в double
    psrldq xmm5, 4        ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5
    movss xmm3, xmm5        ; помещаем четвертое число в xmm3
    cvtss2sd xmm3, xmm3   ; Преобразуем из float в double

    mov rdi, format_str
    call printf

    add rsp, 8
    ret

Опять же применяем для вывода функцию printf. Числа с плавающей точкой передаются в функцию через регистры xmm0-xmm7. Нам надо передать 4 числа, соответственно регистры xmm0-xmm3 будут заняты, а вектор загружается в регистр xmm5.

Обработчка каждого отдельного числа вектора требует нескольких операций. Сначала помещаем отдельное число с плавающей точкой в целевой регистра

movss xmm0, xmm5

Затем преобразуем значение:

cvtss2sd xmm0, xmm0

Затем для перехода к следующему числу в векторе сдвигаем содержимое xmm5 на 4 байта вправо:

psrldq xmm5, 4

Результат работы программы:

root@Eugene:~/asm; nasm -f elf64 hello.asm -o hello.o
root@Eugene:~/asm; gcc -static hello.o -o hello
root@Eugene:~/asm; ./hello
1.2, 2.3, 3.4, 4.5
root@Eugene:~/asm;

Аналогичный пример на Windows:

global main

extern printf

section .data
align 16
nums dd 1.2, 2.3, 3.4, 4.5

format_str db "%.1f, %.1f, %.1f, %.1f", 10, 0

section .text
main: 
    sub rsp, 40
    movaps xmm5, [rel nums]     ; вектор num0 в регистр xmm5

    movss xmm1, xmm5        ; помещаем первое число в xmm0
    cvtss2sd xmm1, xmm1   ; Преобразуем из float в double
    movq rdx, xmm1         ; дублируем в rdx

    psrldq xmm5, 4        ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5
    movss xmm2, xmm5        ; помещаем второе число в xmm1
    cvtss2sd xmm2, xmm2   ; Преобразуем из float в double
    movq r8, xmm2          ; дублируем в r8

    psrldq xmm5, 4        ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5
    movss xmm3, xmm5        ; помещаем третье число в xmm2
    cvtss2sd xmm3, xmm3   ; Преобразуем из float в double
    movq r9, xmm3         ; дублируем в r9

    psrldq xmm5, 4        ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5
    movss xmm4, xmm5        ; помещаем четвертое число в xmm3
    cvtss2sd xmm4, xmm4   ; Преобразуем из float в double
    movq [rsp+32], xmm4    ; помещаем в стек

    mov rcx, format_str
    mov rax, 4
    call printf

    add rsp, 40
    ret

Если бы мы использовали вектор чисел с плавающей точкой двойной точности (по 8 байт), то преобразования нам естественно были бы не нужны. Пример для Linux:

global main

extern printf

section .data
nums dq 2.3, 4.5
format_str db "%.1f, %.1f", 10, 0

section .text
main: 
    sub rsp, 8
    movaps xmm5, [nums]     ; вектор num0 в регистр xmm5

    movsd xmm0, xmm5        ; помещаем первое число в xmm0
    psrldq xmm5, 8          ; сдвигаем вправо на 8 байт - к следующему числу в векторе в xmm5
    movsd xmm1, xmm5        ; помещаем второе число в xmm1

    mov rdi, format_str
    call printf

    add rsp, 8
    ret

Сдвиг чисел в векторе

Но также есть ряд инструкций, которые сдвигают по отдельным битам причем не весь регистр в целом, а отдельные элементы в векторе в регистре. То есть каждый элемент вектора сдвигается на определенное количество бит. Это инструкции:

  • (v)psllw: сдвигает влево слово (16-разрядное целое число)

  • (v)pslld: сдвигает влево двойное слово (32-разрядное целое число)

  • (v)psllq: сдвигает влево четверное слово (64-разрядное целое число)

  • (v)psrlw: сдвигает вправо слово

  • (v)psrld: сдвигает вправо двойное слово

  • (v)psrlq: сдвигает вправо четверное слово

  • (v)psraw: арифметический сдвиг слова вправо

  • (v)psrad: арифметический сдвиг двойного слова вправо

  • (v)psraq: арифметический сдвиг четверного слова вправо

Синтаксис инструкций на примере (v)psllw (остальные инструкции принимают те же операнды)

psllw xmmdest, imm8
psllw xmmdest, xmmsrc/mem128
vpsllw xmmdest, xmmsrc, imm8
vpsllw xmmdest, xmmsrc, mem128
vpsllw ymmdest, ymmsrc, imm8
vpsllw ymmdest, ymmsrc, xmm/mem128

В инструкции с двумя операндами первый операнд представляет сдвигаемое значение, а второй операнд указывает на количество битов для сдвига (это либо 8-битная непосредственная константа, либо регистр XMM, либо в 128-битная переменная).

В инструкции с тремя операндами второй операнд представляет сдвигаемое значение, а третий операнд представляет количество битов для сдвига. Результат сдвига помещается в первый операнд. Исходный регистр (второй операнд) остается неизменным (если, конечно, инструкция не указывает один и тот же регистр для исходного и целевого операндов).

Например, загрузим вектор из 4 чисел и сдвинем каждое число на 1 разряд влево (фактически умножим на 2) в программе на Linux:

global main

extern printf

section .data
nums dd 1, 2, 4, 8
format_str db "%d, %d, %d, %d", 10, 0

section .text
main: 
    sub rsp, 8
    movaps xmm0, [nums]     ; вектор num0 в регистр xmm5

    pslld xmm0, 1         ; сдвиг влево каждого числа из xmm0

    movd esi, xmm0        ; помещаем первое число в esi
    psrldq xmm0, 4        ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm0
    movd edx, xmm0        ; помещаем первое число в edx
    psrldq xmm0, 4        ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm0
    movd ecx, xmm0        ; помещаем первое число в ecx
    psrldq xmm0, 4        ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm0
    movd r8d, xmm0        ; помещаем первое число в r8d

    mov rdi, format_str
    call printf

    add rsp, 8
    ret

Компиляция и результат работы программы:

root@Eugene:~/asm; nasm -f elf64 hello.asm -o hello.o
root@Eugene:~/asm; gcc -static hello.o -o hello
root@Eugene:~/asm; ./hello
2, 4, 8, 16
root@Eugene:~/asm;
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850