Использование инструкций SSE - PullRequest
28 голосов
/ 25 февраля 2009

У меня есть цикл, написанный на C ++, который выполняется для каждого элемента большого целочисленного массива. Внутри цикла я маскирую некоторые биты целого числа, а затем нахожу минимальное и максимальное значения. Я слышал, что если я использую инструкции SSE для этих операций, они будут выполняться намного быстрее по сравнению с обычным циклом, написанным с использованием побитового И и условий if-else. У меня вопрос, должен ли я пойти по этим инструкциям SSE? Кроме того, что произойдет, если мой код работает на другом процессоре? Это все еще будет работать или эти инструкции относятся к конкретному процессору?

Ответы [ 15 ]

24 голосов
/ 25 февраля 2009
  1. Инструкции SSE зависят от процессора. Вы можете посмотреть, какой процессор поддерживает какую версию SSE в Википедии.
  2. Если код SSE будет быстрее или нет, зависит от многих факторов: Первый, конечно, связан с проблемой памяти или ЦП. Если шина памяти является узким местом, SSE мало поможет. Попробуйте упростить целочисленные вычисления, если это ускоряет выполнение кода, возможно, оно связано с процессором, и у вас есть хорошие шансы ускорить его.
  3. Имейте в виду, что написание SIMD-кода намного сложнее, чем написание C ++ -кода, и что результирующий код гораздо сложнее изменить. Всегда держите код C ++ в актуальном состоянии, вы будете хотеть его в качестве комментария и проверять правильность вашего ассемблерного кода.
  4. Подумайте об использовании библиотеки, такой как IPP, которая реализует обычные низкоуровневые операции SIMD, оптимизированные для различных процессоров.
15 голосов
/ 25 февраля 2009

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

Проблемы:

1 Если путь кода зависит от обрабатываемых данных, SIMD становится намного сложнее в реализации. Например:

a = array [index];
a &= mask;
a >>= shift;
if (a < somevalue)
{
  a += 2;
  array [index] = a;
}
++index;

не так просто сделать как SIMD:

a1 = array [index] a2 = array [index+1] a3 = array [index+2] a4 = array [index+3]
a1 &= mask         a2 &= mask           a3 &= mask           a4 &= mask
a1 >>= shift       a2 >>= shift         a3 >>= shift         a4 >>= shift
if (a1<somevalue)  if (a2<somevalue)    if (a3<somevalue)    if (a4<somevalue)
  // help! can't conditionally perform this on each column, all columns must do the same thing
index += 4

2 Если данные не являются достоверными, то загрузка данных в инструкции SIMD обременительна

3 Код зависит от процессора. SSE работает только на IA32 (Intel / AMD), и не все процессоры IA32 поддерживают SSE.

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

10 голосов
/ 26 февраля 2009

Этот тип проблемы является прекрасным примером того, где необходим хороший профилировщик низкого уровня. (Что-то вроде VTune). Это может дать вам гораздо более осознанное представление о том, где находятся ваши горячие точки.

Я предполагаю, что из того, что вы описываете, является то, что ваша горячая точка, вероятно, будет ошибкой предсказания ветвления в результате расчетов min / max с использованием if / else. Поэтому использование встроенных функций SIMD должно позволить вам использовать инструкции min / max, однако, возможно, стоит попробовать вместо этого использовать расчет без ветвей min / max. Это может достичь большинства результатов с меньшим количеством боли.

Примерно так:

inline int 
minimum(int a, int b)
{
  int mask = (a - b) >> 31;
  return ((a & mask) | (b & ~mask));
}
6 голосов
/ 25 февраля 2009

Если вы используете инструкции SSE, вы явно ограничены процессорами, которые их поддерживают. Это означает x86, начиная с Pentium 2 или около того (не могу точно вспомнить, когда они были представлены, но это давно)

SSE2, который, насколько я помню, предлагает целочисленные операции, несколько новее (Pentium 3? Хотя первые процессоры AMD Athlon их не поддерживали)

В любом случае, у вас есть два варианта использования этих инструкций. Либо напишите весь блок кода в ассемблере (возможно, это плохая идея. Это делает практически невозможным для вашего компилятора оптимизировать ваш код, и человеку очень трудно написать эффективный ассемблер).

В качестве альтернативы, используйте встроенные функции, доступные в вашем компиляторе (если память служит, они обычно определяются в xmmintrin.h)

Но опять же, производительность может не улучшиться. Код SSE предъявляет дополнительные требования к данным, которые он обрабатывает. В основном следует помнить, что данные должны быть выровнены по 128-битным границам. Также должно быть мало или вообще не должно быть зависимостей между значениями, загруженными в один и тот же регистр (128-битный регистр SSE может содержать 4 дюйма. Добавление первого и второго регистров не является оптимальным. Но добавление всех четырех значений к соответствующим 4 другой регистр будет быстрым)

Может быть соблазнительно использовать библиотеку, которая оборачивает все низкоуровневые перемены SSE, но это также может разрушить любое потенциальное повышение производительности.

Я не знаю, насколько хороша целочисленная поддержка операций SSE, так что это также может быть фактором, который может ограничивать производительность. SSE в основном нацелен на ускорение операций с плавающей запятой.

4 голосов
/ 25 февраля 2009

Если вы собираетесь использовать Microsoft Visual C ++, вам следует прочитать это:

http://www.codeproject.com/KB/recipes/sseintro.aspx

3 голосов
/ 11 июля 2010

Я могу сказать из моего опыта, что SSE обеспечивает огромное (в 4 раза и более) ускорение по сравнению с простой версией кода c (без встроенного asm, без встроенных функций), но оптимизированный вручную ассемблер может побить сборку, сгенерированную компилятором, если компилятор не может понять, что задумал программист (поверьте, компиляторы не охватывают все возможные комбинации кода и никогда не будут). Да, и компилятор не может каждый раз размещать данные, которые он запускает с максимально возможной скоростью. Но вам нужно много опыта для ускорения по сравнению с Intel-компилятором (если это возможно).

3 голосов
/ 25 февраля 2009

Мы реализовали некоторый код обработки изображений, аналогичный тому, что вы описываете, но в байтовом массиве, в SSE. Ускорение по сравнению с кодом C значительно, в зависимости от точного алгоритма более чем в 4 раза, даже в отношении компилятора Intel. Однако, как вы уже упоминали, у вас есть следующие недостатки:

  • Портативность. Код будет работать на каждом Intel-подобном процессоре, а также на AMD, но не на других процессорах. Это не проблема для нас, потому что мы контролируем целевое оборудование. Переключение компиляторов и даже на 64-битную ОС также может быть проблемой.

  • У вас крутая кривая обучения, но я обнаружил, что после того, как вы поймете принципы, написание новых алгоритмов не так уж сложно.

  • ремонтопригодность. Большинство программистов на C или C ++ не знают ассемблера / SSE.

Я советую вам пойти на это, только если вам действительно нужно улучшение производительности, и вы не можете найти функцию для вашей проблемы в библиотеке, такой как Intel IPP, и если вы можете жить с проблемами переносимости .

2 голосов
/ 27 февраля 2009

Напишите код, который поможет компилятору понять, что вы делаете. GCC будет понимать и оптимизировать код SSE, такой как этот:

typedef union Vector4f
{
        // Easy constructor, defaulted to black/0 vector
    Vector4f(float a = 0, float b = 0, float c = 0, float d = 1.0f):
        X(a), Y(b), Z(c), W(d) { }

        // Cast operator, for []
    inline operator float* ()
    { 
        return (float*)this;
    }

        // Const ast operator, for const []
    inline operator const float* () const
    { 
        return (const float*)this;
    }

    // ---------------------------------------- //

    inline Vector4f operator += (const Vector4f &v)
    {
        for(int i=0; i<4; ++i)
            (*this)[i] += v[i];

        return *this;
    }

    inline Vector4f operator += (float t)
    {
        for(int i=0; i<4; ++i)
            (*this)[i] += t;

        return *this;
    }

        // Vertex / Vector 
        // Lower case xyzw components
    struct {
        float x, y, z;
        float w;
    };

        // Upper case XYZW components
    struct {
        float X, Y, Z;
        float W;
    };
};

Только не забудьте указать в параметрах сборки -msse -msse2!

2 голосов
/ 25 февраля 2009

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

При этом, возможно, не стоит тратить свое время на изучение SSE-кодирования, если вы уже не знакомы с ассемблером на x86 - проще было бы проверить документацию по вашему компилятору и посмотреть, есть ли варианты, позволяющие компилятору автогенерация кода SSE для вас. Некоторые компиляторы очень хорошо справляются с векторизацией циклов. (Вы, вероятно, не удивлены, узнав, что компиляторы Intel хорошо справляются с этой задачей:)

1 голос
/ 30 ноября 2011

Я согласен с предыдущими постерами. Преимущества могут быть довольно большими, но для их получения может потребоваться много работы. Документация Intel по этим инструкциям превышает 4K страниц. Вы можете попробовать EasySSE (библиотека обёрток c ++ поверх встроенных функций + примеры) бесплатно от Ocali Inc.

Я предполагаю, что моя связь с этим EasySSE очевидна.

...