О компиляторах и процессорах
В комментариях к моему посту о целых числах и плавающей точке мне посоветовали обратить внимание на векторные типы данных и сравнить их по производительности со скалярными типами (ну и ручным SSE-ассемблером тоже).
Я обратил и сравнил, но в процессе получилась масса побочных результатов (разные архитектуры, разные компиляторы), которые жалко выкинуть, а хочется опубликовать.
Код
Я развлекался с кодом пересчета цветов по матричному профилю, только с матричной его частью, без пересчета гаммы. Оригинал был взят из dcraw.c и работает с unsigned short входными данными. Удален код, который обрезал значения при выходе за диапазон unsigned short, поэтому цифры производительности местами сильно отличаются от приведенных в предыдущем посте.
void sdotp(unsigned short *image,int sz)
{
unsigned short *img = image;
float out[3];
for(int i = 0; i < sz; i++,img+=4)
{
out[0] = out[1] = out[2] = 0;
for(int cc=0;cc<4;cc++) {
out[0] += mat[0][cc] * img[cc];
out[1] += mat[1][cc] * img[cc];
out[2] += mat[2][cc] * img[cc];
}
for(int cc=0;cc<3;cc++)
img[cc] = out[cc];
img[3] = 0;
}
}
{
float res[4] = {0,0,0,0};
int i,cc;
for(i=0;i<sz;i++)
{
float *dd = &data[i*4];
#pragma unroll
for(cc=0; cc<4; cc++)
{
res[0] += mat[0][cc]*dd[cc];
res[1] += mat[1][cc]*dd[cc];
res[2] += mat[2][cc]*dd[cc];
}
dd[0] = res[0];
dd[1] = res[1];
dd[2] = res[2];
dd[3] = res[3];
}
}
Тот же код, написанный на SSE4.1-intrinsics (то, что матрица преобразования должна быть выровнена на 16 байт - опускаю т.к. эти атрибуты для разных компиляторов пишутся по-разному, отчего код получается громоздким):
{
__m128 m0 = _mm_load_ps(mat[0]);
__m128 m1 = _mm_load_ps(mat[1]);
__m128 m2 = _mm_load_ps(mat[2]);
for(int i=0;i<sz;i++)
{
__m128 d0 = _mm_load_ps(&data[i*4]);
__m128 r0 = _mm_dp_ps(d0,m0,0xf1);
__m128 t0 = _mm_dp_ps(d0,m1,0xff);
__m128 t1 = _mm_dp_ps(d0,m2,0xff);
r0 = _mm_blend_ps(r0,t0,2);
r0 = _mm_blend_ps(r0,t1,4);
_mm_store_ps(&data[i*4],r0);
}
}
Векторный код, тут без громоздких атрибутов никак не обойтись:
__v4sf xmat[3] = {{ 0.17f,0.22f,0.33f,0.44f},{0.55f,0.66f,0.77f,0.88f},{1.01f,1.02f,1.03f,1.04f}} ;
union v4u
{
__v4sf v;
float s[4];
};
void fdotp_vec (float *d, int sz)
{
__v4sf *data = (__v4sf *)d,dd;
v4u rr0,rr1,rr2,res;
for(int i=0;i<sz;i++)
{
dd = data[i];
rr0.v = dd * xmat[0];
rr1.v = dd * xmat[1];
rr2.v = dd * xmat[2];
res.s[0] = rr0.s[0]+rr0.s[1]+rr0.s[2]+rr0.s[3];
res.s[1] = rr1.s[0]+rr1.s[1]+rr1.s[2]+rr1.s[3];
res.s[2] = rr2.s[0]+rr2.s[1]+rr2.s[2]+rr2.s[3];
res.s[3] = 0.0f;
data[i] = res.v;
}
}
Результаты
Вышеупомянутые куски кода
- Компилировались имеющимися на данной системе/процессоре компиляторами:
- Intel Core i7-2600k @4.5GHZ: на машине стоят винды, использовались компиляторы Intel C++ (12.0), Microsoft (2010) и gcc (4.6.2/x64 из MinGW-w64). Всем компиляторам сказано генерировать код для AVX, с максимальной оптимизацией.
- Intel Core i7-920 @3GHz: на машине Linux/x64, тестировался Intel C++ 12.0 и gcc 4.6.2. Код для corei7/-msse4.2.
- Intel Core2 Quad @2.5GHz (Q9300 т.е. Yorkfield-6M): на машине FreeBSD/x64, использовались компиляторы gcc 4.6.2 и clang 3.0 (поверх llvm 3.0/svn rev.133062). Код для core2/sse4.1
- Кормились 100 млн. пикселей 10 раз. Времена исполнения каждого сниппета получаются в диапазоне 3.5-20 секунд, что более чем достаточно.
- Время измерялось через gettimeofday() на Unix и через QueryPerformanceCounter на Windows.
- Мегапикселях в секунду (больше - лучше). Количественное сравнение, очевидно, имеет смысл только внутри одного процессора.
- Процессорных клоках на пиксель (меньше - лучше; в пикселе - 4 компонента на входе и 3 на выходе). Интересно в первую очередь для ручного ассемблерного кода: для разных поколений результат отличается очень сильно.
Компилятор | Megapixels/sec | clocks/pixel | ||||||
---|---|---|---|---|---|---|---|---|
sdotp() | fdotp() | fdotp_sse() | fdotp_vec() | sdotp() | fdotp() | fdotp_sse() | fdotp_vec() | |
Core i7-2600K @4.5GHz/Win7 x64 | ||||||||
gcc 4.6.2 | 115 | 324 | 536 | 172 | 39.1 | 13.9 | 8.4 | 26.2 |
Intel C++ 12.0 | 212 | 261 | 468 | — | 21.2 | 17.2 | 9.6 | — |
MSVC++ 2010 | 211 | 320 | 558 | — | 21.3 | 14.1 | 8.1 | — |
Core i7-920 @3GHz/Linux x64 | ||||||||
Intel C++ 12.0 | 113 | 138 | 190 | 134 | 26.5 | 21.7 | 15.8 | 22.4 |
gcc 4.6 | 97 | 155 | 296 | 104 | 30.9 | 19.4 | 10.1 | 28.8 |
Core2 Quad Q9300 @2.5GHz/FreeBSD x64 | ||||||||
gcc 4.6 | 97 | 134 | 166 | 95 | 25.8 | 18.7 | 15.1 | 26.3 |
clang 3 | 101 | 135 | 166 | 133 | 24.8 | 18.5 | 15.1 | 18.8 |
Обсуждение результатов
Целочисленные вычисления и FP-mode
Как мы видим из таблицы, для AVX-процессоров есть кардинальная разница в скорости "целочисленного" (sdotp) кода между gcc (медленно) и Intel/MSVC++ (почти вдвое быстрее). Рассмотрение ассемблерного листинга выявило интересное:
- Код (в смысле набора использованных инструкций) примерно одинаковый: читаем, конвертируем в плавучку, умножаем-складываем (скалярно: [v]mulss/[v]addss), конвертируем результат обратно в short, сохраняем.
- При этом, gcc очень прямолинеен: порядок следования инструкции повторяет порядок развернутого цикла (цикл развернули все, естественно): грузим первое значение из памяти, считаем, грузим второе, считаем, третье... а потом все сразу сохраняем.
- Intel/MSVC для AVX куда менее прямолинейны: load/store аккуратно размешаны по коду, так что ждать загрузки второго-третьего значения коду не приходится.
Удивительное явление наблюдается на системе Corei7-920: на "целых числах" она не быстрее по абсолютному быстродействию, чем Core2 (а по клокам на пиксель - медленнее). При этом на SSE-коде (fdotp_sse, см. ниже) она ожидаемо быстра (10 клоков на пиксель), таким образом списать проблемы на "тормоза памяти" или вообще "тормоза системы" не получается. Попытка генерации кода под более старый core2 ситуацию не улучшает. Загадка.
В процессе экспериментов выяснилось, что для процессоров Intel/Microsoft огромное влияние на скорость "целочисленного" (sdotp) кода оказывает опция floatint-point mode (/fp: под Windows, -fp-model под Linux). Для "плавающих" реализаций существенной разницы нет.
Рассмотрение ассемблера показало, что причины замедления для /fp:strict очень разные:
- Intel C++ проверяет промежуточные результаты на предмет корректности (не получился ли по дороге NaN).
- MSVC++ делает промежуточную конверсию из целого в плавучку, подозревая что значения в обрабатываемом массиве могли поменяться во время исполнения предыдущих команд.
Для gcc опция -ffast-math практического влияния не оказывает: код немножко разный (разный порядок инструкций), а скорость исполнения - в пределах погрешности между разными запусками.
В таблице выше - все времена для быстрой математики.
Ручной SSE-код
Для ручного SSE-кода (fdotp_sse) хочется отметить три интересных факта.
1. Эффективность SSE-движка от Core2 до Core i7-2 очень выросла: Core2 в наилучшем случае обрабатывает пиксель за 15 тактов, Core i7 (первый) - 10 тактов на пиксель, Core i7-AVX - 8 тактов на пиксель. И это помимо роста реальной тактовой частоты с 3-3.5 (до которых реально разогнать Core2) до 4.5 на которых работает Sandy Bridge. Этот же эффект для С-шного кода гораздо менее выражен, хотя тоже есть.
2. Заметно меньшая эффективность компилятора Intel для "ручного кода" на AVX-процессоре объясняется банально: для load/store используется [v]movups (невыровненые данные) вместо [v]movaps. Эксперименты с __declspec(align(16)) положительного результата не дали, заставить сгенерировать [v]movaps не получилось. Документация гласит однозначно: _mm_load_ps() транслируется в movaps, _mm_loadu_ps() - в movups, однако ж...
Впрочем, в комментах нам подсказывают, что на новых горшках movups/movaps работают одинаково (э... наверное, при прочих равных? может же так получиться, что для невыровненого значения нужно две транзакции по памяти). Получается, что, как и в случае ниже, разница в обработке условий завершения цикла, но отчего такая разница?
3. На Core i7 Intel тоже медленнее, но я не понимаю почему: сгенерированный код для плавучки одинаковый (в обоих случаях, и для gcc и для intel - с movaps). Разница в проверке условий цикла: у intel в начале и "честное" (сравнивается со значением, загружаемым из памяти каждый раз), у gcc - в конце цикла и оптимизированное. Разница в скорости - полтора раза, не должно быть так много.
Плавающая точка
Если вкратце, то Intel - сосет (на обоих рассмотренных системах). Причем, и в loop unrolling (версия, развернутая руками на intel-компиляторе ощутимо быстрее, чем просто #pragma unroll, а для gcc, к примеру, разницы вовсе нет) и вообще в генерации кода.
Почему оно вдруг так - мне непонятно. Все компиляторы пытаются как-то оптимизировать load/store и вычисления (размазать load по коду), только вот интеловский компилятор делает это как-то особенно прямолинейно и неудачно. Скажем, только у Intel я вижу три умножения регистр-память подряд, тот же gcc больше двух подряд не делает, разбавляет операциями регистр-регистр.
Получается удивительно: для целочисленного случая Intel размешивал load/store и вычисления нормально, даже лучше всех, а для плавучки (и очень похожего кода) это умение у него испортилось. А с gcc - наборот. Флаги компиляции при этом одинаковые для разных фрагментов, все бенчмарки считаются одним запуском исполняемого файла.
Векторные расширения
Вся опупея была затеяна ради проверки векторных расширений gcc (как выяснилось, кроме gcc их поддерживают и clang и Linux-компилятор Intel; вероятно Intel C++ для Windows версия Intel C++ тоже поддерживает что-то подобное, но разбираться я уже не стал).
Практика показала, что штука совершенно беспонтовая: в лучшем случае (Intel C++ и clang) достигается производительность скалярного кода (т.е. происходит автоматическая векторизация скалярного кода). В случае gcc производительность на 30-40 процентов меньше, чем у скалярного кода.
Понятно, что использованный пример - не лучший для векторных типов т.к.на три векторных умножения (12 элементарных операций) мы имеем еще 8 скалярных , ну так что ж теперь? Для совсем прямолинейного случая компилятор векторизует сам, только подскажи...
Итого
- Мои изначально оптимистические заявления, дескать перепишите
с целых чисел на плавающую точку и с C++ на ручной ассемблер - и станет впятеро быстрее, оказались несколько преувеличенными. Для упрощенного
кода, без явного клиппинга значений в диапазон 0-65к, разница впятеро достигается только на AVX и только на компиляторе, генерирующем неудачный
целочисленный код для оного AVX. На более старых архитектурах разница может быть вообще всего в 1.6-1.7 раза.
Впрочем, если вернуть клиппинг, то на Core2 мы опять имеем 2.5 раза разницы и это без ручного разворачивания ассемблерного кода в 2-4 раза.
- Компиляторы - разные (открыл америку, да!). В частности, для меня было удивительным узнать, что Intel C++ быстрее gcc для "целочисленного" случая,
медленнее - для плавучки и ассемблерных вставок, а для "векторных расширений" - опять быстрее. Разница - в десятки процентов, это много. Для AVX
и целочисленного случая - еще больше, почти вдвое. Ну, может быть, AVX-кодогенератор у gcc еще просто не отрос.
Я к тому клоню, что вынесение хот-спотов в отдельные куски кода и компиляция этих кусков другим компилятором - может быть не бессмысленным делом, те же два раза gcc/Intel для AVX - это аргумент.
- Правильный выбор форматов данных - это уже половина дела. Как помним, все началось вообще с развенчания идеи о том, что в целых числах - быстрее. Ну и, понятное дело, надо делать удобно процессору (компилятору): выравнивание, писание таким образом, чтобы оно само векторизовалось.
- Ну а дальше обычный цикл оптимизации: пишем какой-то код, хороший профайлер (который на время исполнения мало влияет), поиск хот-спотов, и вот хот-споты и заслуживают оптимизации. Особенно в библиотеках, понятно что обработку параметров у main() можно и не оптимизировать.
Comments
Интересная статья. #pragma
Интересная статья.
#pragma unroll понимает Visual C++? Сомневаюсь.
В документации нет. На
В документации нет. На неизвестную прагму - ругается.
По факту (ассемблерному листингу) - цикл развернут.
Я с удивлением недавно
Я с удивлением недавно заметил что VC++ автоматически анролит циклы с небольшим константным количеством итерации. Возможно он и без прагмы цикл unroll'ит :-).
хороший пост, есть и цифры, и анализ. конечно грустно, что
хороший пост, есть и цифры, и анализ.
конечно грустно, что до сих пор нельзя написать portable код (векторный), который сравнился бы с intrinsics по производительности. изобретать велосипед для каждого набора команд не очень хочется.
для больших вычислительных задач я бы попробовал Intel ArBB, но для "кусочной" оптимизации он не сильно подходит.
Ну так "производительность не переносится". Ну разве только
Ну так "производительность не переносится".
Ну разве только в виде библиотек, заранее написанных на ассемблере, вроде IPP.
Я тут, к удивлению своему, обнаружил, что SSE4.1 (в виде dpps и blendps) есть не на всех Core2. На макбуке 4-летнем (2007-й) процессор T7500, а на нем только SSE3.
Заметно меньшая эффективность
Заметно меньшая эффективность компилятора Intel для "ручного кода" на AVX-процессоре объясняется банально: для load/store используется [v]movups (невыровненые данные) вместо [v]movaps.
Так на i7 вроде без разницы movups или movaps (всмысле по скорости). Я по-моему даже тест делал, хотя могу и ошибаться.
quick google показал: http://www.google.ru/url?sa=t&source=web&cd=9&ved=0CGsQFjAI&url=http%3A%...
"Intel SSE included movups (on previous IA-32 and Intel 64 processors,
movups) was not the best way to load data from memory. Now, on
Intel Core i7 processors, movups and movupd are as fast as movaps
and movapd on aligned data. So, when the Intel Compiler uses /
QxSSE4.2 it removes the if condition to check for aligned data
speeding up loops and reducing the number of instructions to
implement."
Ну вот я смотрю в книгу в
Ну вот я смотрю
в книгув листинг, вижу разницуа) в movups/movaps
б) в обработке итераций цикла (как и для i7-1)
Ну не должна быть такая разница от инвариантов. Хрен его знает, конечно.
OpenCL
> Кормились 1 миллиардом пикселей 10 раз. Времена исполнения каждого сниппета получаются в диапазоне 3.5-20 секунд, что более чем достаточно.
Так и чешутся руки переписать на OpenCL. Просто помоему идеально ложится, если удастся как можно больше сделать операций на GPU до пересылки результата обратно на CPU.
Я читал в предыдущем посте почему это не сделано.
OpenCL выиграет только если
OpenCL выиграет только если надо "10 раз".
Intrinsics
Ссори за оффтоп. Жаль нету компиляторов/утилит который бы по C коду генерировал бы черновой векторизованный вариант для нужной версии SSE для дальнейшей ручной оптимизации. Такой код бы был хорошо портируемый и с хорошей производительностью, хотя скорее всего не с оптимальной.
А чем просто ассемблерный
А чем просто ассемблерный вывод нехорош?
Ну и ispc есть, для векторного случая (т.е. сильно векторизуемых данных) - в самый раз.
Его на intrinsic'и
Его на intrinsic'и переписывать не очень удобно, мне приходиться для многих инструкций по справке искать ее название в intrinsic'ах.
Iscp хорош, надеюсь в нем уже исправили багу что при target SSE2 в аутпут попадали и SSE4 инструкции.
Ну да, оно все раздражает. И
Ну да, оно все раздражает. И трансляция из регистров в имена переменных и разный порядок операндов в командах у gcc и Intel/MS.
С vex-командами (трехадресными) вообще начало сносить крышу:
vdpps xmm11, xmm5, xmm0, 241
против
vdpps $241, %xmm3, %xmm0, %xmm1
Блин.
Жаль нету компиляторов/утилит
Жаль нету компиляторов/утилит который бы по C коду генерировал бы черновой векторизованный вариант для нужной версии SSE для дальнейшей ручной оптимизации
gcc -S
или просто disasm - берите и пилите...
Так и приходится, только в
Так и приходится, только в Visual C++. Долго переписывать с assembler'а на intrinsic'и. Где-то видел скрипт на питоне преобразующий ассемблерные вставки на intrinsic'и но что то найти его никак не могу.
Такой код бы был хорошо
Такой код бы был хорошо портируемый и с хорошей производительностью, хотя скорее всего не с оптимальной.
intrinsic вроде у каждого компилятора свои, или нет?
Покрайней мере у ICC и Visual
Покрайней мере у ICC и Visual C++ они одинаковы, про gcc незнаю точно но мне кажется тоже самое.
gcc понимает _mm (и набор
gcc понимает _mm (и набор #include тот же).
Они транслируются в _builtin_ через дефайны.
Update: и clang - тоже.
Т.е. fdotp_sse из поста - транслировалась всеми четырьмя компиляторами (и давала правильный результат!)
Ну круто, а то asm встраивать
Ну круто, а то asm встраивать везде по своему надо (AT&T/etc). В VisualStudio x64 asm вообще нельзя встраивать в код, приходится отдельно линковать..
Лично мне, векторный код gcc
Лично мне, векторный код gcc кажеться кривоватым.
Ну, нафига ты вводил новый union тип v4u...... Ты же обгадил всю малину для gcc оптимизатора.
А к елементам __v4sf можна обратиться просто по индексу без этих костелей. Правда эта фичя только в 4.6.0 появилась.
typedef float __v4sf __attribute__ ((__vector_size__ (16)));
__v4sf v;
v[0] = 1.0;
Да как-то не вижу я ее в
Да как-то не вижу я ее в 4.6.2:
__v4sf rr0,rr1,rr2,res;
....
res[0] = rr0[0]+rr0[1]+rr0[2]+rr0[3];
Дает: dotp.cpp:172:7: error: invalid types '__v4sf {aka __vector(4) float}[int]' for array subscript
Возможно, я готовлю как-то не так, примеров на вебе как-то совсем мало вменяемых.
$ gcc -v
gcc version 4.6.2 20110826 (prerelease) (FreeBSD Ports Collection)
Вот clang так понимает, только разницы никакой:
dotp_vec(): 130.9 Mpix/sec (15283.4 msec)
dotp_vec2(): 130.6 Mpix/sec (15312.4 msec)
(_vec2 - это без union)
М-да. У меня gcc 4.6.1 и
М-да. У меня gcc 4.6.1 и брать елемент по индексу в векторе умеет.
На моем Core2 Quad Q9500 @ 2.83GHz, 64bit
С и без union-ом код разный получаеться
С union v4u
.L3:
movaps (%rdi,%rax), %xmm0
movl $0x00000000, -12(%rsp)
movaps xmat(%rip), %xmm1
mulps %xmm0, %xmm1
movaps %xmm1, -72(%rsp)
movaps xmat+16(%rip), %xmm1
mulps %xmm0, %xmm1
mulps xmat+32(%rip), %xmm0
movaps %xmm1, -56(%rsp)
movaps %xmm0, -40(%rsp)
movss -72(%rsp), %xmm0
addss -68(%rsp), %xmm0
addss -64(%rsp), %xmm0
addss -60(%rsp), %xmm0
movss %xmm0, -24(%rsp)
movss -56(%rsp), %xmm0
addss -52(%rsp), %xmm0
addss -48(%rsp), %xmm0
addss -44(%rsp), %xmm0
movss %xmm0, -20(%rsp)
movss -40(%rsp), %xmm0
addss -36(%rsp), %xmm0
addss -32(%rsp), %xmm0
addss -28(%rsp), %xmm0
movss %xmm0, -16(%rsp)
movaps -24(%rsp), %xmm0
movaps %xmm0, (%rdi,%rax)
addq $16, %rax
cmpq %rsi, %rax
jne .L3
Без union-а код, как по мне, получше, но черт его знает как быстро инструкция extractps отрабатывает.
.L8:
movaps (%rdi,%rax), %xmm1
movl $0x00000000, -12(%rsp)
movaps xmat(%rip), %xmm3
movaps xmat+16(%rip), %xmm2
mulps %xmm1, %xmm3
mulps %xmm1, %xmm2
mulps xmat+32(%rip), %xmm1
extractps $1, %xmm3, %edx
movaps %xmm3, %xmm0
movd %edx, %xmm4
addss %xmm4, %xmm0
extractps $2, %xmm3, %edx
movd %edx, %xmm4
extractps $3, %xmm3, %edx
movd %edx, %xmm3
extractps $1, %xmm2, %edx
addss %xmm4, %xmm0
movd %edx, %xmm4
extractps $2, %xmm2, %edx
addss %xmm3, %xmm0
movd %edx, %xmm3
extractps $3, %xmm2, %edx
movss %xmm0, -24(%rsp)
movaps %xmm2, %xmm0
addss %xmm4, %xmm0
movd %edx, %xmm4
extractps $1, %xmm1, %edx
movd %edx, %xmm2
extractps $2, %xmm1, %edx
addss %xmm3, %xmm0
movd %edx, %xmm3
extractps $3, %xmm1, %edx
addss %xmm4, %xmm0
movd %edx, %xmm4
movss %xmm0, -20(%rsp)
movaps %xmm1, %xmm0
addss %xmm2, %xmm0
addss %xmm3, %xmm0
addss %xmm4, %xmm0
movss %xmm0, -16(%rsp)
movaps -24(%rsp), %xmm0
movaps %xmm0, (%rdi,%rax)
addq $16, %rax
cmpq %rsi, %rax
jne .L8
В итоге я предлагаю переписать функцию, вот таким вот образом. Все заметно быстрее начинает бегать.
typedef float __v4sf __attribute__ ((__vector_size__ (16)));
__v4sf xmat2[4] = {{0.17f, 0.55f, 1.01f, 0.0f}, {0.22f, 0.66f, 1.02f, 0.0f}, {0.33f, 0.77f, 1.03f, 0.0f}, {0.44f, 0.88f, 1.04f, 0.0f}}; // weights are transposed
void fdotp_vec3 (__v4sf *d, int sz)
{
__v4sf rr0,rr1,rr2,rr3;
for(int i=0;i
Здесь и без слов понятно, что все на порядок быстрее
.L12:
movaps (%rdi,%rax), %xmm1
movaps xmat2(%rip), %xmm0
movaps xmat2+16(%rip), %xmm2
mulps %xmm1, %xmm0
mulps %xmm1, %xmm2
addps %xmm2, %xmm0
movaps xmat2+32(%rip), %xmm2
mulps %xmm1, %xmm2
mulps xmat2+48(%rip), %xmm1
addps %xmm2, %xmm0
addps %xmm1, %xmm0
movaps %xmm0, (%rdi,%rax)
addq $16, %rax
cmpq %rsi, %rax
jne .L12
Кстати, я также думаю что первый проход по массиву пикселей будет всегда медленней всех последующих. Было бы хорошо проверить.
Да, с транспонированной
Да, с транспонированной матрицей конечно должно быть быстрее. Я пощупаю и расскажу.
А проход по инициализированным (т.е. без page fault) пикселям должен быть примерно одинаковый каждый раз, массив *много* больше кэша (для гигапикселя - на три порядка).
Только так, как вы
Только так, как вы предлагаете - ничего не получится, увы.
Ну вот представьте, что у нас в первых 4 элементах d содержится {1,2,3,4}.
Первый элемент результата будет равен:
Вот так вот можно: __m128 x0
Вот так вот можно:
__m128 x0 = _mm_broadcast_ss(&q[i*4]);
__m128 x1 = _mm_broadcast_ss(&q[i*4+1]);
__m128 x2 = _mm_broadcast_ss(&q[i*4+2]);
__m128 x3 = _mm_broadcast_ss(&q[i*4+3]);
__m128 r0 = _mm_mul_ps(x0,m0);
__m128 r1 = _mm_mul_ps(x1,m1);
__m128 r2 = _mm_mul_ps(x2,m2);
__m128 r3 = _mm_mul_ps(x3,m3);
__m128 t1 = _mm_add_ps(r0,r1);
__m128 t2 = _mm_add_ps(r2,r3);
__m128 t3 = _mm_add_ps(t1,t2);
_mm_stream_ps(&q[i*4],t3);
Получается быстрее, чем через dpps (что прикольно, инструкций то больше, да и broadcast этот...), но медленнее чем dpps+avx.
Да, именно это я и
Да, именно это я и предпологал. Писал я генератор псевдослучайных floats и там как раз скалярное произведение. Обрадовался я, и побежал sse4.1 использовать, а на практике оказалось на 15% медленее вручную прописанных mult_ps, add_ps. Но это на core2 было, подумал я что на i7 могли уже допилить до ума. А нет...
В таком духе можно и 256bit вариант написать. Должен быть самый быстрый.
Заврался я совсем sse4.1 на
Заврался я совсем sse4.1 на core2 не было. Это был i5.
Большое спасибо за
Большое спасибо за приведенные измерения.
Вот еще какой вопрос меня волнует. У меня железа нет такого, чтобы проверить.
Как измениться производительность intrinsic варианта на Core-i7, если поменять
_mm_dp_ps на _mm256_dp_ps
_mm_blend_ps на _mm_blend256_ps
То-есть насколько вырастить производительность если мы совсем на AVX переедем и будет обрабатывать по 8 float за проход? А то слухи разные ходят... от 0% до 200% роста.
Ага, действительно
Ага, действительно интересно.
Я был уверен, что AVX-вариант умножает вектора длиной 8, но посмотрел в reference - нет, две половинки по 4.
Сделаю обязательно. Плюс, сделаю обязательно векторный вариант с транспонированными матрицами, как 256-битный, так и 128-битный.
Надо бы еще clang/llvm под виндой развернуть....
Сделал:
Сделал: http://blog.lexa.ru/2011/09/04/o_kompilyatorakh_i_protsessorakh_avx.html
Спасибо. Очень интересно В
Спасибо. Очень интересно В закладки