Оптимизация под набор команд SSE3 и SSE4 не дала вообще никакого прироста производительности. Все три варианты выполняются за стабильно одинаковое количество тактов. А скалярная версия под FPU чуть-учть быстрее.
То ли я чего не так делаю, то ли все дело в процессоре... Но в общем пока я пришел к выводу, что оптимизация операций с плавающей точкой на основе SSE не дает особого выигрыша в производительности. Лучше оптимизировать скалярный код.
Наверное, гораздо больший выигрыш можно получить при выполнении операций с целыми числами в режиме SIMD. Такой вариант я пока не проверял, потому что сильнее всего загружает процессор при выполнении сжатия именно вейвлет-преобразование, которое требует операций с плавающей точкой.
Еще можно попробовать использовать команды SSE для вычислений в скалярном режиме. Возможно, это даст какой-то выигрыш.
Спасибо за интересный эксперимент. Повычислял и я скалярное произведение.
ОтветитьУдалитьДано: 1000 пар 4-х компонентных векторов (float), нужно посчитать скалярное произведение каждой пары (результат 1000 float-ов) 1 миллион раз:
clock_t start = clock();
for (int i = 0; i < 1000000; i++)
{
for (int j = 0; j < 1000; j++) {
c[j] += dot_product(a[j], b[j]); //dot_product - inline function
}
}
double el_time = (double)(clock() - start);
Процессор: i7-4870HQ @ 2.50GHz (MBP 15 Mid 2014)
Компилятор: Apple LLVM version 7.0.0 (clang-700.1.76)
Инструкции AVX не использовал, asm напрямую тоже, только “dpps” через intrinsic clang.
Компилятор порадовал и огорчил одновременно: в режиме O3 (максимальная оптимизация) генерирует понятный и ожидаемый код используя xmmX регистры, “огорчил” тем, что я так и не смог заставить его не использовать их, -mnosse ключика уже нет.
Тесты:
1. Вектор: массив из 4-х float, произведение считаем как a[0] * b[0] + ...
Время: 1662.27 ms (100%)
asm (генерированный): загрузить 2 вектора в xmm0 и xmm1 и дальше 16-ю sse инструкциями посчитать скалярное произведение активно используя sse маски и вспомогательные регистры xmm2-xmm4
2. #1, но я вручную раскрутил цикл по 4, время ~ тоже
3. Используем dpps инструкцию SSE4:
Время: 2297.00 ms (+38%)
asm: загрузить один вектор в xmm0, дальше dpps со 2-м вектором из памяти и к результату добавляет скалярное произведение из предыдущей итерации (я суммирую каждую итерацию, чтобы компилятор не выкинул результат). Компилятор решил раскрутить цикл по 2, но почему-то использует одни и теже регистры!
4. Поможем компилятору #3 и вручную раскрутим цикл по 4:
Время: 1613.17 ms (-3%), перезапускал несколько раз всегда быстрее чем #2, но всего на 3-4%
asm: загрузить 4 вектора в xmm0-xmm3, далее 4-ре dpps с векторами в памяти, дальше 3-мя sse инструкциями упаковали 4-ре 128-bit скалярных произведения (результаты dpps) в один 128-bit регистр и прибавили к результату прошлой итерации.
5. Дальше я ломал голову как заставить clang не использовать 128-bit регистры, ничего не нашел, тогда решил “обмануть” его и разбить вектора на 2 * 4 массива float-в, дабы он не грузил весь вектор одной sse инструкцией.
Время: 410.16 ms! (в 4 раза быстрее чем #1)
Смотрим генерированный asm: компилятор меня перехитрил, он по прежнему грузит в xmmX, но теперь вычисляет 4-ре скалярных произведения за 11 простых sse инструкций! Вот так :)
Судя по статьям в инете, там действительно все не так однозначно. Если я правильно понял то основной плюс SSE & AVX - это загрузка кучи данных за раз. Процессор то быстро считать умеет, а тот же кэш все еще жуткий тормоз, чем больше из него возьмем тем лучше...
Ну, похоже и этого плюса нет. FPU у меня в скалярном режиме считает чуть быстрее.
УдалитьДа и данные не всегда лежат в одной строчке, иногда их приходится собирать.
Выигрыш, по идее, должен быть в том, что четыре операции с плавающей точкой выполняются как одна.
Но! Обычные скалярные инструкции, если они независимы, тоже на современных процессорах могут выполняются одновременно.
Также, ходят слухи, что на самом деле для выполнения операций с плавающей точкой SSE использует ровно тот же вычислительный блок, что и FPU. Тогда, если процессор суперскалярный и FPU инструкции выписаны в независимом порядке, то выигрыша от SSE не будет вообще.
Правда, есть очень интересный момент с целочисленными операциями. Возможно, здесь SSE дает прикурить.
Может из-за сбора и упаковки в 128-битный регистр не получается получить выигрыш? Основное время уходит на общение с кэшем и памятью?
УдалитьВроде современный процессор и sse инструкций выполняет 2 за один так (одно сложение и одно умножение), покрайней мере у меня раскручивание цикла ускоряло работу.
Не знаю. Я эксперименты свои проводил на готовом векторе, там уже собирать нечего, просто загрузить из памяти в регистр.
УдалитьВ документации по оптимизации рекомендуют для максимальной производительности для некоторых SSE инструкций (не помню уже, для каких) после выполнения такой инструкции до обращения к ресурсам SSE выполнить не менее 10 обычных инструкций (также забыл, только общего назначения или можно и FPU использовать). Сейчас уже не найду то место документации, конечно.