Использование инструкций SSE
У меня есть цикл, написанный на C++, который выполняется для каждого элемента большого целочисленного массива. Внутри цикла я маскирую некоторые биты целого числа, а затем нахожу минимальное и максимальное значения. Я слышал, что если я буду использовать инструкции SSE для этих операций, они будут выполняться намного быстрее по сравнению с обычным циклом, написанным с использованием побитового И и условий if-else. У меня вопрос, должен ли я пойти по этим инструкциям SSE? Кроме того, что произойдет, если мой код работает на другом процессоре? Это все еще будет работать или эти инструкции относятся к конкретному процессору?
15 ответов
- Инструкции SSE зависят от процессора. Вы можете посмотреть, какой процессор поддерживает какую версию SSE в Википедии.
- Если код SSE будет быстрее или нет, зависит от многих факторов: Первый, конечно, связан с проблемой памяти или ЦП. Если шина памяти является узким местом, SSE мало поможет. Попробуйте упростить ваши целочисленные вычисления, если это делает код быстрее, он, вероятно, привязан к процессору, и у вас есть хорошие шансы ускорить его.
- Имейте в виду, что написание SIMD-кода намного сложнее, чем написание C++-кода, и что результирующий код гораздо сложнее изменить. Всегда держите код C++ в актуальном состоянии, вы будете хотеть его в качестве комментария и проверять правильность вашего ассемблерного кода.
- Подумайте об использовании библиотеки, такой как 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). Это может дать вам гораздо более осознанное представление о том, где находятся ваши горячие точки.
Я предполагаю, что из того, что вы описываете, является то, что ваша горячая точка, вероятно, будет ошибкой предсказания ветвления, возникающей в результате расчетов 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));
}
Если вы используете инструкции SSE, вы явно ограничены процессорами, которые их поддерживают. Это означает, что x86 восходит к Pentium 2 или около того (точно не помню, когда они были представлены, но это давно)
SSE2, который, насколько я помню, предлагает целочисленные операции, несколько новее (Pentium 3? Хотя первые процессоры AMD Athlon их не поддерживали)
В любом случае, у вас есть два варианта использования этих инструкций. Либо напишите весь блок кода в ассемблере (возможно, это плохая идея. Это делает практически невозможным для компилятора оптимизировать ваш код, и человеку очень трудно написать эффективный ассемблер).
В качестве альтернативы, используйте встроенные функции, доступные в вашем компиляторе (если память служит, они обычно определяются в xmmintrin.h)
Но опять же, производительность может не улучшиться. Код SSE предъявляет дополнительные требования к данным, которые он обрабатывает. В основном следует помнить, что данные должны быть выровнены по 128-битным границам. Также должно быть мало или вообще не должно быть зависимостей между значениями, загруженными в один и тот же регистр (128-битный регистр SSE может содержать 4 дюйма. Добавление первого и второго регистров не является оптимальным. Но добавление всех четырех значений в соответствующие 4 дюйма в другой реестр будет быстрым)
Может быть соблазнительно использовать библиотеку, которая оборачивает все низкоуровневые перемены SSE, но это также может разрушить любое потенциальное повышение производительности.
Я не знаю, насколько хороша поддержка целочисленных операций SSE, так что это также может быть фактором, который может ограничить производительность. SSE в основном нацелена на ускорение операций с плавающей запятой.
Если вы собираетесь использовать Microsoft Visual C++, вам следует прочитать это:
Из своего опыта я могу сказать, что SSE обеспечивает огромное (в 4 раза и более) ускорение по сравнению с простой версией кода c (без встроенного asm, без встроенных функций), но оптимизированный вручную ассемблер может превзойти сгенерированную компилятором сборку, если компилятор может ' не понять, что задумал программист (поверьте, компиляторы не охватывают все возможные комбинации кода и никогда не будут). Да, и компилятор не может каждый раз размещать данные, которые он запускает с максимально возможной скоростью. Но вам нужно много опыта для ускорения по сравнению с Intel-компилятором (если это возможно).
Мы реализовали некоторый код обработки изображений, аналогичный тому, что вы описываете, но в байтовом массиве, в SSE. Ускорение по сравнению с кодом C значительно, в зависимости от точного алгоритма более чем в 4 раза, даже в отношении компилятора Intel. Однако, как вы уже упоминали, у вас есть следующие недостатки:
Переносимость. Код будет работать на каждом Intel-подобном процессоре, а также на AMD, но не на других процессорах. Это не проблема для нас, потому что мы контролируем целевое оборудование. Переключение компиляторов и даже на 64-битную ОС также может быть проблемой.
У вас крутая кривая обучения, но я обнаружил, что после того, как вы поймете принципы, написание новых алгоритмов не так уж сложно.
Ремонтопригодность. Большинство программистов на C или C++ не знают ассемблера /SSE.
Я советую вам пойти на это только в том случае, если вам действительно нужно улучшение производительности, и вы не можете найти функцию для вашей проблемы в библиотеке, такой как Intel IPP, и если вы можете жить с проблемами переносимости.
Инструкции 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 в ваших параметрах сборки!
Я согласен с предыдущими постерами. Преимущества могут быть довольно большими, но для их получения может потребоваться много работы. Документация Intel по этим инструкциям превышает 4K страниц. Вы можете попробовать EasySSE (библиотека обёрток C++ поверх встроенных функций + примеры) бесплатно от Ocali Inc.
Я предполагаю, что моя связь с этим EasySSE ясна.
Просто вкратце добавим к тому, что было сказано ранее о различных версиях SSE, доступных на разных процессорах: это можно проверить, посмотрев соответствующие флаги функций, возвращаемые инструкцией CPUID (подробности см., Например, в документации Intel).
Хотя верно то, что SSE специфичен для некоторых процессоров (SSE2 может быть относительно безопасным, SSE2 намного меньше по моему опыту), вы можете обнаружить CPU во время выполнения и динамически загружать код в зависимости от целевого CPU.
Посмотрите на встроенный ассемблер для C/C++, вот статья DDJ. Если вы не уверены на 100%, что ваша программа будет работать на совместимой платформе, вы должны следовать рекомендациям, приведенным здесь.
Встроенные функции SIMD (такие как SSE2) могут ускорить подобные вещи, но для правильного использования необходимы знания и опыт. Они очень чувствительны к выравниванию и задержке конвейера; неосторожное использование может сделать производительность еще хуже, чем было бы без них. Вы получите намного более простое и более быстрое ускорение от простого использования предварительной выборки из кэша, чтобы убедиться, что все ваши целые находятся в L1 вовремя, чтобы вы могли работать с ними.
Если вашей функции не требуется пропускная способность, превышающая 100 000 000 целых чисел в секунду, SIMD, вероятно, не стоит ваших проблем.
Я не рекомендую делать это самостоятельно, если вы не достаточно опытны в сборке. Как отмечает Skizz, использование SSE, скорее всего, потребует тщательной реорганизации ваших данных, и в лучшем случае выгода часто сомнительна.
Возможно, для вас было бы гораздо лучше написать очень маленькие циклы и держать ваши данные очень плотно организованными, и просто полагаться на то, что компилятор сделает это за вас. И компилятор Intel C, и GCC (начиная с версии 4.1) могут автоматически векторизовать ваш код и, вероятно, будут работать лучше, чем вы. (Просто добавьте -ftree-vectorize к своим CXXFLAGS.)
Изменить: Еще одна вещь, которую я должен упомянуть, это то, что несколько компиляторов поддерживают встроенные ассемблеры, которые, вероятно, IMO, будет проще в использовании, чем синтаксис asm() или __asm {}.