SSE (Streaming SIMD Extension) - набор инструкций, позволяющий выполнять несколько одинаковых операций одновременно. Набор инструкций SSE продолжает расширятся.
Для хранения аргументов операций SSE используются регистры xmm. 32-битная система команд x86 позволяет использовать 8 регистров %xmm0 ... %xmm7. 64-битная система команд x64 позволяет использовать 16 регистров %xmm0 ... %xmm15. Регистры xmm являются scratch-регистрами, то есть при вызове подпрограмм сохранение значений не гарантируется (как с регистрами %eax, %ecx, %edx).
Регистры xmm имеют размер 128 бит и могут хранить 2 64-битных, 4 32-битных целых или вещественных значения, а также 8 16-битных или 16 8-битных целых значения. Интерпретация битового содержимого регистров xmm зависит от выполняемой инструкции.
В стандартном соглашении о вызовах x64 первые 8 параметров вещественных типов float или double передаются на регистрах %xmm0 ... %xmm7, последующие аргументы передаются в стеке. Результат вещественного типа возвращается в регистре %xmm0.
В стандартном соглашении о вызовах x32 аргументы вещественных типов передаются на стеке. Специального выравнивания для double не требуется. Результат вещественного типа возвращается в регистре FPU %st(0). Даже если результат в %st(0) не используется вызывающей программой, он должен быть удален из стека FPU. Если в коде x86 для вычислений используется SSE, а подпрограмма должна вернуть значение вещественного типа, результат из SSE должен быть скопирован на верхушку стека FPU.
Например, для копирования значения типа double на FPU может использоваться следующая последовательность операций:
sub $8, %esp // резервируем память
movsd %xmm0, (%esp) // копируем значение double из %xmm0 в стек
fldl (%esp) // загружаем из стека на %st(0)
add $8, %esp // очищаем стек
Регистры SSE можно использовать для обычных вычислений с плавающей точкой. Такие инструкции по терминологии Intel называются скалярными. В этом случае в регистрах xmm будет использоваться только младшая часть: младшие 32 или 64 бита.
Для пересылки скалярных значений могут использоваться следующие инструкции:
movsd SRC, DST // пересылка между регистрами xmm и памятью значения double
movss SRC, DST // пересылка значения типа float
Эти инструкции позволяют пересылать значение из регистра xmm в другой регистр xmm, а также между регистрами xmm и памятью. При обращении к памяти на x86 достаточно, чтобы значение double было выровнено по адресу, кратному 4.
Со скалярными значениями поддерживаются следующие операции:
addsd SRC, DST // DST += SRC, double
addss SRC, DST // DST += SRC, float
subsd SRC, DST // DST -= SRC, double
subss SRC, DST // DST -= SRC, float
mulsd SRC, DST // DST *= SRC, double
mulss SRC, DST // DST *= SRC, float
divsd SRC, DST // DST /= SRC, double
divss SRC, DST // DST /= SRC, float
sqrtsd SRC, DST // DST = sqrt(SRC), double
sqrtss SRC, DST // DST = sqrt(SRC), float
maxsd SRC, DST // DST = max(SRC, DST), double
maxss SRC, DST // DST = max(SRC, DST), float
minsd SRC, DST // DST = min(SRC, DST), double
minss SRC, DST // DST = min(SRC, DST), float
Преобразование double->int выполняется инструкцией
cvtsd2si SRC, DST // DST = (int32_t) SRC
Здесь SRC - регистр xmm или память, DST - 32-битный регистр общего назначения. Инструкция выполняет преобразование вещественног числа типа double в 32-битное знаковое целое число.
Преобразование double->float выполняется инструкцией:
cvtsd2ss SRC, DST // DST = (float) SRC
Преобразование int->double выполняется инструкцией:
cvtsi2sd SRC, DST // DST должен быть регистр xmm, SRC либо GPR, либо память
Преобразование float->double:
cvtss2sd SRC, DST // DST = (double) SRC
Для преобразований float->int и int->float предназначены инструкции cvtss2si и cvtsi2ss.
Сравнение двух скалярных значений типа float или double выполняется инструкцией:
comisd SRC, DST // DST - SRC, double
comiss SRC, DST // DST - SRC, float
В результате выполнения операции сравнения устанавливаются флаги PF, CF, ZF. Флаг PF устанавливается, если результат - неупорядочен. Флаг ZF устанавливается, если значения равны. Флаг CF устанавливается, если DST < SRC. Для условного перехода после сравнения можно использовать условные переходы для беззнаковых чисел. Например, ja будет выполнять условный переход, если DST > SRC.
Векторные вычисления в терминологии Intel описываются как вычисления с упакованными (packed) значениями.
Для пересылки 128-битных значений между памятью и регистрами xmm и между двумя регистрами xmm используется инструкция
movapd SRC, DST // DST = SRC
если один из аргументов - память, адрес должен быть выровнен по адресу, кратному 16. Для пересылки по невыровненным адресам можно использовать инструкцию movupd.
С векторными значениями поддерживаются следующие операции, которые выполняются одновременно со всеми значениями в регистрах (2 для double или 4 для float):
addpd SRC, DST // DST += SRC, double
addps SRC, DST // DST += SRC, float
subpd SRC, DST // DST -= SRC, double
subps SRC, DST // DST -= SRC, float
mulpd SRC, DST // DST *= SRC, double
mulps SRC, DST // DST *= SRC, float
divpd SRC, DST // DST /= SRC, double
divps SRC, DST // DST /= SRC, float
sqrtpd SRC, DST // DST = sqrt(SRC), double
sqrtps SRC, DST // DST = sqrt(SRC), float
maxpd SRC, DST // DST = max(SRC, DST), double
maxps SRC, DST // DST = max(SRC, DST), float
minpd SRC, DST // DST = min(SRC, DST), double
minps SRC, DST // DST = min(SRC, DST), float
Обычная операция над упакованными SSE-регистрами может рассматриваться как "вертикальная". Например,
рассмотрим инструкцию ADDPS A, B
. Эта инструкция складывает четыре float-значения в операнде A
с соответствующими 4 значениями в операнде B и кладет результат в операнд B. Если A и B рассматривать
как массивы из 4 значений типа float, то операция может быть описана следующим образом:
float A[4];
float B[4];
B[0] = A[0] + B[0]
B[1] = A[1] + B[1]
B[2] = A[2] + B[2]
B[3] = A[3] + B[3]
В противовес "вертикальной" операции "горизонтальная" операция вовлекает соседние значение в одном регистре.
Например, инструкция HADDPS A, B
выполняется следующим образом:
float A[4];
float B[4];
B[0] = B[0] + B[1];
B[1] = B[2] + B[3];
B[2] = A[0] + A[1];
B[3] = A[2] + A[3];