SSE — это… Что такое SSE?

Преобразование чисел с плавающей точкой (x87) в целые числа

В эту подгруппу входит одна-единственная инструкция из всего набора Prescott New Instructions (SSE3), которая работает на уровне x87 FPU.

FISTTP (сохранение целочисленного значения с освобождением элемента стека x87-FP с округлением в сторону нуля). Ее поведение аналогично поведению стандартной IA-32 инструкции FISTP, но важным отличием является использование округления в сторону нуля (известного как truncate или chop) вне зависимости от того, какой способ округления выбран в данный момент в контрольном слове FPU.

Вычисления с комплексными числами

Комплексная арифметика довольно часто встречается во всевозможных спектральных задачах, в частности — задачах обработки аудиоданных. К ним относятся дискретное/быстрое преобразование Фурье (DFT/FFT), частотная фильтрация и т.п. Среди новых расширений SSE3 можно выделить пять инструкций, позволяющих ускорить вычисления с комплексными числами.

Сюда относятся инструкции одновременного сложения-вычитания ADDSUBPS и ADDSUBPD и инструкции дублирования данных MOVSLDUP, MOVSHDUP и MOVDDUP (исходный операнд которых может представлять собой адрес памяти). Первые позволяют исключить излишние операции смены знака у части элементов данных (обычно осуществляемые с помощью XORPS/XORPD), вторые — лишние операции распаковки данных, загружаемых из памяти (UNPCKLPS/UNPCKLPD, SHUFPS/SHUFPD).

Рассмотренные ниже примеры кода показывают, как можно реализовать умножение комплексных чисел, используя только SSE2 (код 2.1), или SSE2 и новые расширения SSE3 (код 2.2). mem_X представляет собой превый комплексный операнд, mem_Y — второй; результат умножения сохраняется в mem_Z. В регистре XMM7 хранится константа, используемая для смены знака одного из элемента данных.

Загрузка невыровненных переменных

Данную подгруппу представляет инструкция LDDQU. Это операция особой загрузки невыровненного 128-битного значения из памяти, исключающая возможность «разрыва» (пересечения границы) строки кэша. В случае, если адрес загружаемого элемента выровнен по 16-байтной границе, LDDQU осуществляет обычную загрузку запрашиваемого 16-байтного значения (т.е., по сути, ведет себя аналогично инструкциям MOVAPS/MOVAPD/MOVDQA из стандартного набора SSE/SSE2).

В противном случае LDDQU загружает целых 32 байта, начиная с выровненного адреса (ниже запрашиваемого) с последующим извлечением требуемых 16 байт. Использование этой инструкции позволяет достичь значительного увеличения производительности при загрузке невыровненных 128-битных значений из памяти, по сравнению со стандартными инструкциями MOVUPS/MOVUPD/MOVDQU SIMD-расширений SSE/SSE2.

Кодирование видео

Наиболее затратной с точки зрения процессорного времени операцией в задачах кодирования видеоданных обычно является Motion Estimation (ME), в которой блоки текущего кадра сравниваются с блоками предыдущего кадра с целью нахождения наилучшего соответствия (критерием последнего обычно является сумма абсолютных разностей).

  • Отсутствие аппаратной поддержки;
  • Возможность «разрыва» (пересечения границы) строки кэша.

Так, в микроархитектуре NetBurst нет микрооперации, соответствующей загрузке невыровненного 128-битного значения (командами MOVUPS, MOVUPD или MOVDQU), в связи с чем последние эмулируются двумя 64-битными операциями загрузки с последующим объединением данных. Помимо эмуляции, могут возникать и дополнительные затраты в том случае, если загрузка сопровождается пересечением 64-байтной границы строки кэша процессора.

Введенная в набор SSE3 инструкция специализированной загрузки невыровненных 128-битных значений LDDQU призвана решить эту проблему. В то же время, поскольку эта команда загружает большее количество данных (32 байта, начиная с выровненного адреса), имеется ряд ограничений на ее использование. В частности, ее не рекомендуется использовать для некэшируемых (Uncached, UC) регионов, или регионов с объединением записи (Write-combining, USWC), а также в ситуациях, когда может ожидаться «загрузка после сохранения» (Store-to-load forwarding, STLF).

В остальных случаях, каковыми является большинство, можно ожидать до 30% улучшения производительности кода, использующего операции невыровненной загрузки (учитывая, что пересечение границы строки кэша при загрузке невыровненных значений может проявляться в 25% случаев). Приведем фрагменты такого кода из алгоритма ME, использующие только SSE2 (код 3.1) и SSE2/SSE3 (код 3.2).

Предлагаем ознакомиться  Процессорозависимость: игровые тесты CPU от Celeron до восьмиядерных Core i7

Синхронизация потоков

В последнюю подгруппу можно включить две инструкции, нацеленные на использование в системном программировании с целью предоставления возможности более эффективной синхронизации потоков, в частности, при использовании технологии Hyper-Threading. Ожидается, что эти инструкции будут использоваться при разработке операционных систем и драйверов устройств с целью улучшения производительности процессора и снижения энергопотребления последнего, когда он находится в режиме «пустого» ожидания (по всей видимости, наряду с введенной в расширения SSE2 инструкцией PAUSE).

MONITOR — устанавливает диапазон адресов памяти (обычно используется одна строка кэша), по которому будет осуществляться отслеживание записей по стандартному протоколу write-back.

MWAIT — вводит логический процессор в оптимизированный режим (режим низкого энергопотребления) при ожидании записей по протоколу write-back по пространству адресов, заданных инструкцией MONITOR. С архитектурной точки зрения ее поведение идентично NOP. Выход из оптимизированного состояния осуществляется в случае записи по установленному пространству адресов, а также при срабатывании любого прерывания или исключения. Использование SSE3 в разработке и оптимизации ПО

Перечислив набор инструкций, вошедших в новый расширения SSE3, остановимся теперь на рассмотрении ряда задач, в которых использование таких расширений способно обеспечить прибавку в производительности. При этом мы попытаемся оценить, какой именно выигрыш в производительности следует ожидать от использования новых SIMD-расширений на практике.

Integer instructions

  • Arithmetic
    • PMULHUW, PSADBW, PAVGB, PAVGW, PMAXUB, PMINUB, PMAXSW, PMINSW
  • Data movement
  • Other

Other instructions

  • MXCSR management
  • Cache and Memory management
    • MOVNTQ, MOVNTPS, MASKMOVQ, PREFETCH0, PREFETCH1, PREFETCH2, PREFETCHNTA, SFENCE

Код 1.1

DWORD cwOld, cwNew;
__asm
{
fld dword ptr[f] // загрузка значения float
fnstcw word ptr[cwOld] // сохранение FPUCW
movzx eax, word ptr[cwOld]
or eax, 0x0c00 // установка режима округления
// в сторону нуля (truncate)
mov dword ptr[cwNew], eax
fldcw word ptr[cwNew] // загрузка нового значения FPUCW
fistp dword ptr[i] // сохранение значения int
fldcw word ptr[cwOld] // восстановление FPUCW
}

Легко заметить, что такая процедура, довольно часто встречающаяся на практике, содержит в себе три явно лишние операции, связанных с сохранением, загрузкой и восстановлением значения контрольного слова x87 FPU. Заметим, что время исполнения каждой из последних измеряется как минимум несколькими тактами процессора (в ряде случаев — десятью и более, в зависимости от конкретной реализации микроархитектуры процессора). С точки зрения оптимизации кода, в рамках существующий архитектуры IA-32 можно наметить два выхода из этой ситуации.

Первый — это сохранить значение контрольного слова FPU, загрузить новое значение контрольного слова FPU с нужным способом округления, после чего совершить сразу целый ряд преобразований, и, наконец, восстановить исходное состояние FPU. Именно такой способ рекомендуется в ряде документации по оптимизации кода для процессоров x86 (в частности, для AMD Athlon).

Второй подход — это использовать доступные SIMD-расширения, вроде SSE или 3DNow! В первом случае для этой цели подходят команды CVTSS2SI и CVTPS2PI (последняя позволяет осуществлять два преобразования одновременно, но использует MMX-регистр, что, в свою очередь, приводит к необходимости переключения режимов FPU/MMX, которое является относительно «бесплатным» далеко не для всех процессоров).

Во втором случае преобразование пары вещественных значений в целочисленные можно осуществлять с помощью команды PF2ID. Здесь вновь присутствуют трудности, присущие, кстати, набору 3DNow! в целом — использование MMX-регистров, и, как следствие, необходимость переключения режимов работы процессора (либо очистки, либо сохранения/восстановления содержимого FPU-стека и MMX-регистров). В качестве наиболее простой процедуры преобразования можно придумать следующую, оформленную в виде ассемблерной вставки:

Код 4.1. скалярное произведение двух векторов, sse


VECTOR4F A, B;
float dot;
// считаем, что четвертый элемент каждого вектора равен нулю
__asm
{
movaps xmm0, xmmword ptr[A] // 0 | A.z | A.y | A.x
movaps xmm1, xmmword ptr[B] // 0 | B.z | B.y | B.x
mulps xmm0, xmm1 // 0 | A.z*B.z | A.y*B.y | A.x*B.x
movhlps xmm1, xmm0 // ? | ? | 0 | A.z*B.z
unpcklps xmm0, xmm0 // A.y*B.y | A.y*B.y | A.x*B.x | A.x*B.x
movhlps xmm2, xmm0 // ? | ? | A.y*B.y | A.y*B.y
addss xmm0, xmm1 // ? | ? | ? | A.x*B.x A.z*B.z
addss xmm0, xmm2 // ? | ? | ? | A.x*B.x A.y*B.y A.z*B.z
movss dword ptr[dot], xmm0
}

В рассмотренном примере вычисление скалярного произведения осуществляется при помощи одной операции умножения (MULPS), двух операций перемещения (MOVHLPS), одной операции распаковки (UNPCKLPS) и двух операций однокомпонентного (скалярного) сложения (ADDSS). Кроме того, что немаловажно, задействуется один дополнительный XMM-регистр.

Предлагаем ознакомиться  7 распространенных ошибок при выборе видеокарты

Выходов из ситуации, в смысле, путей дальнейшей оптимизации кода, как всегда, можно придумать несколько. Первый из них — использовать для вычисления скалярных произведений не SSE, а FPU. Но это связано с дополнительными трудностями переноса данных из регистров SSE в стек FPU и обратно в том случае, если большая часть кода использует именно расширения SSE. В связи с этим мы не будем рассматривать его в нашем сравнительном тестировании производительности.

Второй способ — использовать однокомпонентные (скалярные) инструкции SSE. Получается что-то вроде аналога FPU-кода, но более удобного, поскольку мы используем все те же XMM-регистры:

Код 4.4. скалярное произведение двух векторов, sse/sse3


VECTOR4F A, B;
float dot;
__asm
{
movaps xmm0, xmmword ptr[A] // 0 | A.z | A.y | A.x
movaps xmm1, xmmword ptr[B] // 0 | B.z | B.y | B.x
mulps xmm0, xmm1 // 0 | A.z*B.z | A.y*B.y | A.x*B.x
movhlps xmm1, xmm0 // ? | ? | 0 | A.z*B.z
haddps xmm0, xmm0 // A.z*B.z | A.y*B.y A.x*B.x |
// A.z*B.z | A.y*B.y A.x*B.x
addss xmm0, xmm1 // ? | ? | ? | A.B
movss dword ptr[dot], xmm0
}

По сравнению с первоначальным кодом (код 4.1) мы добились значительного сокращения количества операций, относящихся непосредственно к вычислению скалярного произведения, за счет использования новой инструкции из набора SSE3 — инструкции горизонтального сложения HADDPS. Более того, поскольку HADDPS умеет работать сразу с парой XMM-регистров, само собой напрашивается сделать код еще более оптимальным, вычисляя сразу два скалярных произведения:

Код 4.5. два скалярных произведения, sse/sse3


VECTOR4F A1, A2, B1, B2;
__declspec(align(8)) float dot[2];
__asm
{
movaps xmm0, xmmword ptr[A1] // 0 | A1.z | A1.y | A1.x
movaps xmm1, xmmword ptr[A2] // 0 | A2.z | A2.y | A2.x
movaps xmm2, xmmword ptr[B1] // 0 | B1.z | B1.y | B2.x
movaps xmm3, xmmword ptr[B2] // 0 | B2.z | B2.y | B2.x
mulps xmm0, xmm2 // 0 | A1.z*B1.z | A1.y*B1.y | A1.x*B1.x
mulps xmm1, xmm3 // 0 | A2.z*B2.z | A2.y*B2.y | A2.x*B2.x
haddps xmm0, xmm1 // A2.z*B2.z | A2.y*B2.y A2.x*B2.x |
// A1.z*B1.z | A1.y*B1.y A1.x*B1.x
haddps xmm0, xmm0 // A2.B2 | A1.B1 | A2.B2 | A1.B1
movlps qword ptr[dot], xmm0
}

И все равно, складывается ощущение некоторой незавершенности оптимизации. Два скалярных произведения в результате вычисления как бы дублируются. Действительно, а почему бы не попытаться использовать SSE3 на всю мощь и попробовать посчитать сразу четыре скалярных произведения? Взгляните на следующий код — это достигается уже далеко не такой ценой, как при вычислении этих самых четырех скалярных произведений исключительно с помощью SSE-инструкций (код 4.3):

Код 4.6. четыре скалярных произведения, sse/sse3


VECTOR4F A1, A2, A3, A4, B1, B2, B3, B4;
__declspec(align(16)) float dot[4];
__asm
{
movaps xmm0, xmmword ptr[A1] // 0 | A1.z | A1.y | A1.x
movaps xmm1, xmmword ptr[A2] // 0 | A2.z | A2.y | A2.x
movaps xmm2, xmmword ptr[A3] // 0 | A3.z | A3.y | A3.x
movaps xmm3, xmmword ptr[A4] // 0 | A4.z | A4.y | A4.x
movaps xmm4, xmmword ptr[B1] // 0 | B1.z | B1.y | B2.x
movaps xmm5, xmmword ptr[B2] // 0 | B2.z | B2.y | B2.x
movaps xmm6, xmmword ptr[B3] // 0 | B3.z | B3.y | B3.x
movaps xmm7, xmmword ptr[B4] // 0 | B4.z | B4.y | B4.x
mulps xmm0, xmm4 // 0 | A1.z*B1.z | A1.y*B1.y | A1.x*B1.x
mulps xmm1, xmm5 // 0 | A2.z*B2.z | A2.y*B2.y | A2.x*B2.x
mulps xmm2, xmm6 // 0 | A3.z*B3.z | A3.y*B3.y | A3.x*B3.x
mulps xmm3, xmm7 // 0 | A4.z*B4.z | A4.y*B4.y | A4.x*B4.x
haddps xmm0, xmm1 // A2.z*B2.z | A2.y*B2.y A2.x*B2.x |
// A1.z*B1.z | A1.y*B1.y A1.x*B1.x
haddps xmm2, xmm3 // A4.z*B4.z | A4.y*B4.y A4.x*B4.x |
// A3.z*B3.z | A3.y*B3.y A3.x*B3.x
haddps xmm0, xmm2 // A4.B4 | A3.B3 | A2.B2 | A1.B1
movaps xmmword ptr[dot], xmm0
}

Заметьте, что в этом коде нет ни одной лишней операции (излишнего перемещения данных) и не используется ни один вспомогательный регистр (для временного хранения данных). Более того, данный код одинаково хорошо подходит для вычисления четырех скалярных произведений как трехмерных, так и четырехмерных векторов, в то время как код 4.3 специально «подогнан» и годится лишь для операций с трехмерными векторами.

Предлагаем ознакомиться  Сегодня мы рассмотрим, как именно переключить на ноутбуке видеокарту с intel на nvidia

Как всегда, самое время проверить все вышесказанное на практике. Для этого попробуем оценить, во сколько тактов процессора уложится вычисление одного скалярного произведения с использованием только SSE-команд (код 4.1, 4.2) и SSE вместе с SSE3 (код 4.4), а также сравним эффективность вычисления четырех скалярных произведений с помощью SSE-кода (код 4.3) и кода, в полной мере использующего новые процессорные расширения (код 4.6).

Тип кода Время исполнения, тактов
4.1. Одно скалярное произведение, SSE 8.60
4.2. Одно скалярное произведение, SSE scalar 8.75
4.4. Одно скалярное произведение, SSE/SSE3 9.00
4.3. Четыре скалярных произведения, SSE 33.67
4.6. Четыре скалярных произведения, SSE/SSE3 18.33

Что же мы видим? Одно скалярное произведение, использующее расширения SSE/SSE3 далеко не на полную мощность, не то что не выигрывает, но даже несколько проигрывает при использовании новой инструкции HADDPS из набора SSE3. И это несмотря на то, что мы сократили объем кода и устранили необходимость использования дополнительных XMM-регистров.

Тем не менее, поскольку увеличение времени исполнения такого кода (4.4) является сравнительно малым, использование SSE3 в этом случае (вычисление одного скалярного произведения трехкомпонентных векторов) можно считать оправданным, хотя бы по причине того, что в распоряжении компилятора, ну или непосредственно разработчика, остается большее количество доступных XMM-регистров.

Но давайте теперь посмотрим, что же получается в случае одновременного вычисления сразу четырех скалярных произведений? В этом случае выигрыш в скорости при использовании SSE3 оказывается весьма и весьма значительным, SSE3-код опережает свой SSE-аналог почти на 84%. Что еще раз доказывает, что использование сразу всех четырех элементов XMM-регистра является оптимальным режимом работы SIMD расширений Intel, и SSE3 здесь не является исключением. В этой связи ручная оптимизация под SSE3 может оказаться намного более эффективной, нежели поручение этой же работы оптимизирующему компилятору.

В заключение, представим данные нашего тестирования новых расширений SSE3 в виде одной таблицы. Из нее видно, что в целом расширения SSE3 можно считать удачными, а их использование в разработке и оптимизации ПО — оправданным, поскольку в большинстве случаев оно позволяет получить серьезный выигрыш в производительности.

Тип задачи Выигрыш в скорости, раз, по сравнению с традиционным FPU/SIMD-кодом
Преобразование данных (float to int) 10.4
Комплексное умножение 1.88
Загрузка невыровненных значений 1.40
Одно скалярное произведение векторов 0.96
Четыре скалярных произведения векторов 1.84

Округления

  • ROUND{PS, PD} xmm1, xmm2/m128, imm8 — (Round Packed Single/Double Precision Floating-Point Values)

Округление всех 32/64-битных полей. Режим округления (4 варианта) выбирается либо из MXCSR.RC, либо задаётся непосредственно в imm8. Также можно подавить генерацию исключения потери точности.

  • ROUND{SS, SD} xmm1, xmm2/m128, imm8 — (Round Scalar Single/Double Precision Floating-Point Values)

Округление только младшего 32/64-битного поля (остальные биты остаются неизменными).

Проверки бит

PTEST xmm1, xmm2/m128 — (Logical Compare)Установить флаг ZF, если только в xmm2/m128 все биты помеченные маской из xmm1 равны нулю. Если все не помеченные биты равны нулю, то установить флаг CF. Остальные флаги (AF, OF, PF, SF) всегда сбрасываются. Инструкция не модифицирует xmm1.

Процессоры с поддержкой sse4

  • Intel
  • Penryn (SSE4.1)
  • Nehalem (SSE4.1, SSE4.2)
  • AMD
  • AMD А10, А8 и А6 (SSE4.1,SSE4.2,SSE4A)
  • Bulldozer (SSE4a, SSE4.1, SSE4.2)
  • Zen (SSE4a, SSE4.1, SSE4.2)
  • VIA
  • VIA Nano (SSE4.1)

Скалярное умножение векторов

  • DPPS xmm1, xmm2/m128, imm8 — (Dot Product of Packed Single Precision Floating-Point Values)
  • DPPD xmm1, xmm2/m128, imm8 — (Dot Product of Packed Double Precision Floating-Point Values)

Скалярное умножение векторов (dot product) 32/64-битных полей. Посредством битовой маски в imm8 указывается, какие произведения полей должны суммироваться и что следует прописать в каждое поле результата: сумму указанных произведений или 0.0.

Чтение wc памяти

MOVNTDQA xmm1, m128 — (Load Double Quadword Non-Temporal Aligned Hint)

Операция чтения, позволяющая ускорить (до 7.5 раз) работу с write-combining областями памяти.

Оцените статью
Техничка
Adblock detector