Вопрос

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

Это было полезно?

Решение

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

Другие советы

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 есть много документации.

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

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

Что-то вроде этого:

inline int 
minimum(int a, int b)
{
  int mask = (a - b) >> 31;
  return ((a & mask) | (b & ~mask));
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Напишите код, который поможет компилятору понять, что вы делаете.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 в параметрах сборки!

Хотя это правда, что SSE специфичен для некоторых процессоров (по моему опыту, SSE может быть относительно безопасным, а SSE2 гораздо менее безопасным), вы можете обнаружить ЦП во время выполнения и динамически загружать код в зависимости от целевого ЦП.

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

Если вашей функции не требуется пропускная способность выше 100 000 000 целых чисел в секунду, SIMD, вероятно, не стоит для вас проблем.

Просто добавлю вкратце к тому, что было сказано ранее о том, что разные версии SSE доступны на разных процессорах:Это можно проверить, просмотрев соответствующие флаги функций, возвращаемые инструкцией CPUID (см., например,Подробности в документации Intel).

Посмотри на встроенный ассемблер для C/C++ вот статья ДДД.Если вы не уверены на 100%, что ваша программа будет работать на совместимой платформе, вам следует следовать рекомендациям, которые многие здесь дали.

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

Полагаю, моя принадлежность к EasySSE очевидна.

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

Вероятно, для вас было бы намного лучше писать очень маленькие циклы, сохранять ваши данные очень четко организованными и просто полагаться на то, что компилятор сделает это за вас.И компилятор Intel C, и GCC (начиная с версии 4.1) могут автоматически векторизовать ваш код и, вероятно, справятся с этой задачей лучше, чем вы.(Просто добавьте -ftree-vectorize к вашему CXXFLAGS.)

Редактировать:Еще я должен упомянуть, что некоторые компиляторы поддерживают встроенные функции сборки, который, по моему мнению, будет проще использовать, чем синтаксис asm() или __asm{}.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top