Помощь с умножением ассемблера/SSE

Я пытался понять, как улучшить свой код в очень важной паре строк:

float x = a*b;
float y = c*d;
float z = e*f;
float w = g*h;

все a, b, c... являются числами с плавающей запятой.

Я решил изучить использование SSE, но, похоже, не нашел никаких улучшений, на самом деле он оказался в два раза медленнее. Мой код SSE:

Vector4 abcd, efgh, result;
abcd = [float a, float b, float c, float d];
efgh = [float e, float f, float g, float h];
_asm {
movups xmm1, abcd
movups xmm2, efgh
mulps xmm1, xmm2
movups result, xmm1
}

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

Любые комментарии или помощь будут очень признательны, мне в основном нужно понять, почему мои вычисления с использованием SSE медленнее, чем последовательный код C++?

Я компилирую в Visual Studio 2005 на Windows XP, используя Pentium 4 с HT, если это предоставляет какую-либо дополнительную информацию для помощи.

Заранее спасибо!


person Brett    schedule 02.06.2010    source источник
comment
Я думаю, вам нужно предоставить больше контекста. Простое умножение четырех пар чисел с плавающей запятой займет едва измеримое время на любом современном ПК. Это в цикле? Вы где-то сохраняете результаты или используете их в качестве промежуточных звеньев для следующей итерации?   -  person CB Bailey    schedule 03.06.2010
comment
Я понимаю, что наибольшая выгода от SSE будет состоять в том, чтобы делать много итераций, каждый раз упаковывая регистр, но все, что я планирую делать, это использовать числа, сгенерированные из этого, просто возвращаться к некоторым вызовам сложения и вычитания, ничего которые я хотел бы включить в код SSE, но любое улучшение времени вычислений приведет к значительной экономии времени на протяжении всего жизненного цикла кода.   -  person Brett    schedule 03.06.2010
comment
Это не обязательно верно. Если это не в цикле, то любые преимущества будут совершенно незаметны при любом проходе кода. Конечно, если программное обеспечение используется в течение нескольких тысяч лет, общее сэкономленное время может иметь смысл, но на самом деле это все. Не переоптимизируйте, современные компиляторы очень хороши. Если он на самом деле работает слишком медленно, сначала профилируйте, а затем оптимизируйте узкие места.   -  person Donnie    schedule 03.06.2010
comment
@Donnie: оптимизирующие компиляторы не очень хорошо генерируют код FPU, поскольку многие оптимизации не работают с кодом FPU и могут значительно изменить поведение кода. См. msdn.microsoft.com/en-us/library/aa289157.aspx для получения дополнительной информации.   -  person Skizz    schedule 03.06.2010


Ответы (5)


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

Кроме того, невозможно перемещать данные между SSE и FPU/ALU без использования записи в RAM с последующим чтением. Современные чипы IA32 хорошо справляются с этим конкретным шаблоном (запись, затем чтение), но все же будут делать недействительным некоторый кэш, что будет иметь эффект удара.

Чтобы получить максимальную отдачу от SSE, вам нужно взглянуть на весь алгоритм и данные, которые он использует. Значения a, b, c и d, а также e, f, g и h должны постоянно находиться в этих массивах, чтобы данные не перемещались в памяти до загрузки регистров SSE. Это не просто и может потребовать значительной переработки вашего кода и данных (возможно, вам придется по-другому хранить данные на диске).

Также стоит отметить, что SSE только 32-битный (или 64-битный, если вы используете удвоения), тогда как FPU — 80-битный (независимо от float или double), поэтому вы получите немного разные результаты при использовании SSE по сравнению с использованием FPU. Только вы знаете, будет ли это проблемой.

person Skizz    schedule 02.06.2010
comment
Из того, что я понимаю из вашего ответа, похоже, что я должен пытаться использовать встроенные функции только в том случае, если я могу использовать их для более чем одного расчета, это правильно? И причина этого в том, что я не очень эффективно перемещаю данные самостоятельно? Я не могу постоянно хранить значения a, b, c и d и e, f, g и h в этих массивах, поскольку им нужно загружать текущие значения для каждого вычисления, поэтому мне было бы трудно увидеть выгода? Спасибо за любую помощь! - person Brett; 03.06.2010
comment
@Brett: Да, это в основном так. Вам нужно хранить все в SSE, чтобы действительно получить выгоду. В названии SSE — Streaming SIMD Extensions есть небольшая подсказка. Просто из любопытства, откуда берутся эти ценности, т.е. какова общая картина? - person Skizz; 03.06.2010
comment
Итак, более общая картина состоит в том, что на самом деле это часть матрицы вращения, но я делаю одну матрицу вращения на итерацию через большой цикл, в котором я сравниваю векторы признаков. Из-за структуры я не вижу возможности одновременно подключить кучу вычислений SSE, но даже малейшее преимущество определенно приведет к значительному улучшению времени выполнения моей программы. В качестве альтернативы b, d, f, h являются значениями sin и cos, которые вычисляются на этапе инициализации, и в настоящее время я перехожу к их хранению в выровненных блоках для более быстрого умножения. Спасибо за вашу помощь! - person Brett; 04.06.2010

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

person Anycorn    schedule 02.06.2010
comment
Итак, я проверил то, что, как я думаю, вы говорите, используя команду movups для хранения значений, выровненных в регистре, затем использовал movaps для имитации выровненных данных, и, наконец, это быстрее, чем последовательный код С++, пока я начинаю мой таймер после выравнивания данных. Если я всегда начинаю с невыровненных данных, имеет ли смысл не видеть преимущества SSE/ASM? - person Brett; 03.06.2010

Вы можете включить использование SSE и SSE2 в параметрах программы в более новых версиях VS и, возможно, в 2005 году. Компилировать с использованием экспресс-версии?

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

Кроме того, вам пришлось бы выполнять ОГРОМНЫЙ объем работы для SSE/2, чтобы получить заметную выгоду.

person Puppy    schedule 02.06.2010
comment
Я предполагаю, что меня все еще смущает тот факт, что я получил некоторый рабочий код SSE/2 (у меня было много версий кода, вставленных выше), и на самом деле он работал медленнее, чем мой последовательный код. Достаточно, чтобы моя ~10-секундная программа (написанная полностью последовательно) заняла ~11,5 секунд (только с этой операцией в SSE/2). - person Brett; 03.06.2010

Это старая тема, но я заметил ошибку в вашем примере. Если вы хотите выполнить это:

float x = a*b;
float y = c*d;
float z = e*f;
float w = g*h;

Тогда код должен быть таким:

Vector4 aceg, bdfh, result;  // xyzw
abcd = [float a, float c, float e, float g];
efgh = [float b, float d, float f, float h];
_asm {
movups xmm1, abcd
movups xmm2, efgh
mulps xmm1, xmm2
movups result, xmm1
}

И чтобы получить еще больше скорости, я бы посоветовал вам не использовать отдельный регистр для «результата».

Во-первых, не все алгоритмы выиграют от перезаписи на SSE. Алгоритмы, управляемые данными (например, алгоритмы, управляемые таблицами поиска) плохо переводятся в SSE, потому что много времени тратится на упаковку и распаковку данных в векторы для работы SSE.

Надеюсь, это все еще помогает.

person Stéphane Perras    schedule 18.04.2012

Во-первых, когда у вас есть что-то 128-битное (16-байтовое) выровненное, вы должны использовать MOVAPS, так как это может быть намного быстрее. Компилятор обычно должен давать выравнивание по 16 байт даже в 32-битных системах.

Ваши строки C/C++ не делают то же самое, что и ваш код sse.

Четыре числа с плавающей запятой в одном регистре xmm умножаются на четыре числа с плавающей запятой в другом регистре. Предоставление вам:

float x = a*e;
float y = b*f;
float z = c*g;
float w = d*h;

В sse1 вы должны использовать SHUFPS, чтобы переупорядочить поплавки в обоих регистрах перед умножением.

Также для обработки данных, объем которых превышает кэш процессора, вы можете использовать невременные хранилища (MOVNTPS), чтобы уменьшить загрязнение кеша. Обратите внимание, что невременные хранилища в других случаях работают намного медленнее.

person gens    schedule 18.05.2013