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

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

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

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

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

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

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

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

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

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

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

Пример:

.globl _start

.data
nums0: .long 3, 5, 8, 11

.text
_start:
    movaps nums0, %xmm0     # вектор num0 в регистр %xmm0
    psrldq $4, %xmm0        # сдвигаем регистр %xmm0 вправо на 4 байта
    movd %xmm0, %edi        # %edi = 5

    movq $60, %rax
    syscall

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

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

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

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

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

.globl _start

.data
nums0: .long 254, 5, 8, 11
hexstr128: .ascii "0x123456789ABCDEFG123456789ABCDEFG\n"
len=.-hexstr128    # размер строки
hexmap: .byte 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70

.text
_start:
    movaps nums0, %xmm0     # вектор num0 в регистр %xmm0
    call print_reg128

    movq $60, %rax
    syscall

# функция печати регистра на консоль
# Параметры: XMM0 - выводимое значение 
print_reg128:
    movq $hexmap, %rsi      # таблица преобразования
    movq $hexstr128, %rbx     # начало строки
    addq $33, %rbx         # переходим на первый байт числа
    # в цикле проходим от RCX = 16 до RCX=1 с приращением -1
    movq $16, %rcx  # 16 байтов - 32 символа для печати
forloop128: 
    movq %xmm0, %rax
    andq $0xf, %rax     # накладываем маску - на число в AL
    movb (%rsi, %rax), %al  # находим символьное отображение числа в таблице hexmap 
    movb %al, (%rbx)   # сохраняем один символ ascii

    movq %xmm0, %rax
    shrq $4, %rax       # берем второй полубайт из XMM0
    andq $0xf, %rax     # накладываем маску - на число в AL
    movb (%rsi, %rax), %al  # находим символьное отображение числа в таблице hexmap 
    movb %al, -1(%rbx)   # сохраняем один символ ascii

    subq $2, %rbx   # вычитаем из адреса в RBX единицу для перехода к следующему символу

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

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

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

movq $hexmap, %rsi    # таблица преобразования

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

movq $hexstr128, %rbx     # начало строки
addq $33, %rbx         # переходим на первый байт числа

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

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

movq $16, %rcx  # 16 байт
forloop128: 

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

movq %xmm0, %rax
andq $0xf, %rax     # накладываем маску - на число в AL
movb (%rsi, %rax), %al  # находим символьное отображение числа в таблице hexmap  
movb %al, (%rbx)   # сохраняем один символ ascii

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

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

movq %xmm0, %rax
shrq $4, %rax       # берем второй полубайт из XMM0
andq $0xf, %rax     # накладываем маску - на число в AL
movb (%rsi, %rax), %al  # находим символьное отображение числа в таблице hexmap 
movb %al, -1(%rbx)   # сохраняем один символ ascii

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

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

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

root@Eugene:~/asm# as hello.s -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

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

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

.globl main

.data
nums0: .long 255, 5, 8, 11
format_str: .asciz "%d, %d, %d, %d\n"
.text
main: 
    subq $8, %rsp
    movaps nums0, %xmm0     # вектор num0 в регистр xmm0

    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

Также чтобы получить каждое отдельное число вектора в %xmm0, применяем операцию сдвига вправо и помещаем крайнее правое число вектора в один из регистров для вывода функцией printf. Пример компиляции и работы программы:

root@Eugene:~/asm# gcc -static hello.s -o hello
root@Eugene:~/asm# ./hello
255, 5, 8, 11
root@Eugene:~/asm#

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

Вывод чисел с плавающей точкой чуть сложнее:

.globl main

.data
nums: .single 1.2, 2.3, 3.4, 4.5
format_str: .asciz "%.1f, %.1f, %.1f, %.1f\n"
.text
main: 
    subq $8, %rsp
    movaps nums, %xmm5     # вектор nums в регистр xmm5

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

    movq $format_str, %rdi
    call printf

    addq $8, %rsp
    ret

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

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

movss %xmm5, %xmm0

Затем преобразуем значение .single (.float) в тип .double:

cvtss2sd %xmm0, %xmm0

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

psrldq $4, %xmm5 

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

root@Eugene:~/asm# gcc -static hello.s -o hello
root@Eugene:~/asm# ./hello
1.2, 2.3, 3.4, 4.5
root@Eugene:~/asm#

Если бы мы использовали вектор чисел .double, то преобразования нам естественно были бы не нужны:

.globl main

.data
nums: .double 2.3, 4.5
format_str: .asciz "%.1f, %.1f\n"
.text
main: 
    subq $8, %rsp
    movaps nums, %xmm5     # вектор nums в регистр xmm5

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

    movq $format_str, %rdi
    call printf

    addq $8, %rsp
    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 imm8, xmmdest
psllw xmmsrc/mem128, xmmdest
vpsllw imm8, xmmsrc, xmmdest
vpsllw mem128, xmmsrc, xmmdest
vpsllw imm8, ymmsrc, ymmdest
vpsllw xmm/mem128, ymmsrc, ymmdest

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

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

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

.globl main

.data
nums: .long 1, 2, 4, 8
format_str: .asciz "%d, %d, %d, %d\n"
.text
main: 
    subq $8, %rsp
    movaps nums, %xmm0     # вектор nums в регистр xmm0
    pslld $1, %xmm0         # сдвиг влево каждого числа из xmm0

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

    movq $format_str, %rdi
    call printf

    addq $8, %rsp
    ret

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

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