Войти
ПрограммированиеТермины

Streaming SIMD Extentions (SSE)

Streaming SIMD Extentions (SSE) — набор команд процессора, позволяющие обрабатывать за одну инструкцию сразу большой объём данных. Собственно, SIMD расшифровывается как Single Instruction, Multiple Data.

В программировании игр использует в основном для обработки однотипных данных, вроде векторной математики и software графики.

Ставшие стандартными на момент написания статьи SSE расширения в идеале могут ускорить тяжёлую математику в четыре раза — в одном SSE регистре хранится четыре float32 или int32 числа, операции над которыми проводятся одновременно.

Существует два основных подхода к использованию SSE в своём приложении:
— прописывать руками всю математику, используя расширения команд или так называемые интринсики,
— доверить эту нелёгкую работу оптимизатору кода, если речь идёт о мощных C++ компиляторах.

Как показывает практика, сгенерировать SSE код эффективнее, чем это сделает за вас оптимизатор, действительно непросто. Но если вы всё-таки решитесь взять быка за рога и написать узкое место вашей программы, используя эту технологию, то есть два пути сделать это:

1) Использование ассемблерных вставок в коде. Олдскул метод, самое близкое "общение" с процессором:

float a[] = {0.0f, 1.0f, 2.0f, 3.0f};
float b[] = {4.0f, 5.0f, 6.0f, 7.0f};
float c[4];
_asm
{
  movups xmm0, a //поместить четыре числа с плавающей точкой из массива a в регистр xmm0
  movups xmm1, b //поместить четыре числа с плавающей точкой из массива b в регистр xmm0

  addps xmm1, xmm0 //сложить четыре пары чисел
  movups c, xmm1 //поместить результат в массив c
};

2) Или другой способ, который считается чуть более человеческим — использовать так называемые «интринсики». Интринсик (intrinsics) — это обёртка над теми же самыми SSE функциями, встроенная в компилятор в виде обычных C++ функций. Используются примерно так же, только вместо массивов из четырёх флотов удобнее использовать union'ы из тех же четырёх флотов со специальным 128-битным типом _m128:

struct Vector128
{
  Vector128(float _x, float _y, float _z, float _w)
  {
    x = _x;
    y = _y;
    z = _z;
    w = _w;
  }
  Vector128(__m128 _vec)
  {
    vec = _vec;
  }
  union
  {
    __m128 val;
    struct {float x, y, z, w;};
  };
};
int main()
{
  Vector128 a(0.0f, 1.0f, 2.0f, 3.0f);
  Vector128 b(4.0f, 5.0f, 6.0f, 7.0f);
  Vector128 c(0.0f, 0.0f, 0.0f, 0.0f);
  c.vec = _mm_add_ups(a.vec, b.vec);
}

Очень важная вещь, которую нужно обязательно держать в голове, если вы всё-таки решили собственноручно совладать с SSE — это то, что нужно использовать разные инструкции для данных, выровненных в памяти по 16 байт и всех остальных, с суффиксами соответственно _ups и _ps. Как нетрудно догадаться, первые быстрее, причём на порядок, но обеспечить, чтобы все используемые в программе данные были выровненны, далеко не просто. Эта неприятная особенность часто вызывает ошибки при неверном использовании или убивает производительность при правильном использовании но невыровненых данных.

Также стоит отметить, что при реализации обычной трёхмерной векторной математики нам нужно всего три компоненты, а SSE инструкции обрабатывают их по четыре - казалось бы, ну пусть себе обрабатывает одну лишнюю компоненту, мы её просто использовать не будет. К сожалению, и здесь всё не так солнечно: если все наши векторные данные разрастутся на четверть, на четверть возрастёт и время копирования данных, и время таскания их по шине RAM<->CPU и, что самое главное, возрастёт количество cache miss'ов(кэш промахов) — именно эти факторы обычно сводят плюсы от «ручной» SSE оптимизации у новичков на нет. При недостаточно умелом подходе производительность может не только возрасти, но и упасть.

Что такое Streaming SIMD Extentions (SSE)?

#ASM, #процессоры

10 июня 2009