О компиляторах и процессорах

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

Я обратил и сравнил, но в процессе получилась масса побочных результатов (разные архитектуры, разные компиляторы), которые жалко выкинуть, а хочется опубликовать.

Код

Я развлекался с кодом пересчета цветов по матричному профилю, только с матричной его частью, без пересчета гаммы. Оригинал был взят из dcraw.c и работает с unsigned short входными данными. Удален код, который обрезал значения при выходе за диапазон unsigned short, поэтому цифры производительности местами сильно отличаются от приведенных в предыдущем посте.

float   mat[3][4] = {{ 0.17f,0.22f,0.33f,0.44f},{0.55f,0.66f,0.77f,0.88f},{1.01f,1.02f,1.03f,1.04f}} ;

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;
        }
}
Тот же код, переписанный в плавучке
void fdotp(float *data, int sz)
{
        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];
        }
}
Как показала практика, #pragma unroll понимается всеми компиляторами нормально (оговорка, по следам комментов, про Visual C++: компилятор ругается на неизвестную прагму, а по факту - цикл развернут), версия с циклом (по cc) развернутым вручную в большинстве случаев работает практически с той же скоростью. Хуже всего по этому параметру компилятор Intel, у него unrolled-руками версия на 7% быстрее, для остальных компиляторов разница меньше, для gcc - и вовсе меньше разброса результатов.

Тот же код, написанный на SSE4.1-intrinsics (то, что матрица преобразования должна быть выровнена на 16 байт - опускаю т.к. эти атрибуты для разных компиляторов пишутся по-разному, отчего код получается громоздким):

void fdotp_sse4(float data[], int sz)
{
        __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);
        }

}
Прикол в том, что "ассемблерный" код получился компактнее, чем его выражение на C. Спасибо Интелу за команду скалярного произведения.

Векторный код, тут без громоздких атрибутов никак не обойтись:

typedef float __v4sf __attribute__ ((__vector_size__ (16)));
__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;
  }
}
Как видим, скалярное произведение под векторы ну никак не заточено, неаккуратненько получается. А транспонирования матрицы, которое тут было бы к месту, в этой векторной нотации непонятно как красиво написать (на SSE это пишут в 8 shuffle и 4 временных переменных, но использовать intrinsics совсем не хочется так как потеряется потенциальная совместимость с не-SSE процессорами).

Результаты

Вышеупомянутые куски кода

  • Компилировались имеющимися на данной системе/процессоре компиляторами:
    1. Intel Core i7-2600k @4.5GHZ: на машине стоят винды, использовались компиляторы Intel C++ (12.0), Microsoft (2010) и gcc (4.6.2/x64 из MinGW-w64). Всем компиляторам сказано генерировать код для AVX, с максимальной оптимизацией.
    2. Intel Core i7-920 @3GHz: на машине Linux/x64, тестировался Intel C++ 12.0 и gcc 4.6.2. Код для corei7/-msse4.2.
    3. 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 115324536172 39.113.98.426.2
Intel C++ 12.0 212261468 21.217.29.6
MSVC++ 2010 211320558 21.314.18.1
Core i7-920 @3GHz/Linux x64
Intel C++ 12.0 113138190134 26.521.715.822.4
gcc 4.6 97155296104 30.919.410.128.8
Core2 Quad Q9300 @2.5GHz/FreeBSD x64
gcc 4.6 9713416695 25.818.715.126.3
clang 3 101135166133 24.818.515.118.8

Обсуждение результатов

Целочисленные вычисления и FP-mode

Как мы видим из таблицы, для AVX-процессоров есть кардинальная разница в скорости "целочисленного" (sdotp) кода между gcc (медленно) и Intel/MSVC++ (почти вдвое быстрее). Рассмотрение ассемблерного листинга выявило интересное:

  • Код (в смысле набора использованных инструкций) примерно одинаковый: читаем, конвертируем в плавучку, умножаем-складываем (скалярно: [v]mulss/[v]addss), конвертируем результат обратно в short, сохраняем.
  • При этом, gcc очень прямолинеен: порядок следования инструкции повторяет порядок развернутого цикла (цикл развернули все, естественно): грузим первое значение из памяти, считаем, грузим второе, считаем, третье... а потом все сразу сохраняем.
  • Intel/MSVC для AVX куда менее прямолинейны: load/store аккуратно размешаны по коду, так что ждать загрузки второго-третьего значения коду не приходится.
Примерно тот же самый эффект есть и на более старых архитектурах: Intel и clang чуть менее прямолинейны - и чуть быстрее.

Удивительное явление наблюдается на системе 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++ делает промежуточную конверсию из целого в плавучку, подозревая что значения в обрабатываемом массиве могли поменяться во время исполнения предыдущих команд.
А результат примерно один, fp:strict код у Intel вдвое медленнее, у 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 скалярных , ну так что ж теперь? Для совсем прямолинейного случая компилятор векторизует сам, только подскажи...

Итого

  1. Мои изначально оптимистические заявления, дескать перепишите с целых чисел на плавающую точку и с C++ на ручной ассемблер - и станет впятеро быстрее, оказались несколько преувеличенными. Для упрощенного кода, без явного клиппинга значений в диапазон 0-65к, разница впятеро достигается только на AVX и только на компиляторе, генерирующем неудачный целочисленный код для оного AVX. На более старых архитектурах разница может быть вообще всего в 1.6-1.7 раза.

    Впрочем, если вернуть клиппинг, то на Core2 мы опять имеем 2.5 раза разницы и это без ручного разворачивания ассемблерного кода в 2-4 раза.

  2. Компиляторы - разные (открыл америку, да!). В частности, для меня было удивительным узнать, что Intel C++ быстрее gcc для "целочисленного" случая, медленнее - для плавучки и ассемблерных вставок, а для "векторных расширений" - опять быстрее. Разница - в десятки процентов, это много. Для AVX и целочисленного случая - еще больше, почти вдвое. Ну, может быть, AVX-кодогенератор у gcc еще просто не отрос.

    Я к тому клоню, что вынесение хот-спотов в отдельные куски кода и компиляция этих кусков другим компилятором - может быть не бессмысленным делом, те же два раза gcc/Intel для AVX - это аргумент.

Ну и поймите меня правильно, я не призываю писать все на ассемблере, никоим образом. Идея в другом:
  • Правильный выбор форматов данных - это уже половина дела. Как помним, все началось вообще с развенчания идеи о том, что в целых числах - быстрее. Ну и, понятное дело, надо делать удобно процессору (компилятору): выравнивание, писание таким образом, чтобы оно само векторизовалось.
  • Ну а дальше обычный цикл оптимизации: пишем какой-то код, хороший профайлер (который на время исполнения мало влияет), поиск хот-спотов, и вот хот-споты и заслуживают оптимизации. Особенно в библиотеках, понятно что обработку параметров у main() можно и не оптимизировать.
Получить ускорение в разы на хот-споте вполне реально, а усилий это требует вполне подъемных.

Comments

Интересная статья.
#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)

Ну не должна быть такая разница от инвариантов. Хрен его знает, конечно.

> Кормились 1 миллиардом пикселей 10 раз. Времена исполнения каждого сниппета получаются в диапазоне 3.5-20 секунд, что более чем достаточно.
Так и чешутся руки переписать на OpenCL. Просто помоему идеально ложится, если удастся как можно больше сделать операций на GPU до пересылки результата обратно на CPU.
Я читал в предыдущем посте почему это не сделано.

OpenCL выиграет только если надо "10 раз".

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

А чем просто ассемблерный вывод нехорош?

Ну и ispc есть, для векторного случая (т.е. сильно векторизуемых данных) - в самый раз.

Его на 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 C++ они одинаковы, про gcc незнаю точно но мне кажется тоже самое.

gcc понимает _mm (и набор #include тот же).

Они транслируются в _builtin_ через дефайны.

Update: и clang - тоже.

Т.е. fdotp_sse из поста - транслировалась всеми четырьмя компиляторами (и давала правильный результат!)

Ну круто, а то asm встраивать везде по своему надо (AT&T/etc). В VisualStudio x64 asm вообще нельзя встраивать в код, приходится отдельно линковать..

Лично мне, векторный код 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 и брать елемент по индексу в векторе умеет.
На моем 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}.
Первый элемент результата будет равен:

  • Правильный ответ: 1*0.17 + 2*0.22 + 3*0.33 + 4*0.44 = 3.36
  • Ваш вариант: 1*0.17 + 1*0.22 + 1*0.33 + 1*0.44 = 1.16

Вот так вот можно:

__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 на 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 под виндой развернуть....

Спасибо. Очень интересно В закладки

Add new comment