Опять про movntps

Читал комментарии к прошлому посту про movntps, много думал. Вспоминал, что на i7-920 выигрыш от movntps был и значительный.

Пришлось потратить час-полтора на очередной набор микробенчмарок. Картинка получилась куда сложнее, чем я сначала подумал.

Код

Может быть три легко локализуемых случая:

  • fill - данные как-то производятся из ничего и льются в память;
  • copy - из одного места читаем, в другое пишем;
  • stream - читаем данные, обрабатываем, пишем на старое место.

Соответственно, код для fill:

#ifdef USENTPS
#define STORE(a,b) _mm_stream_ps(a,b)
#else
#define STORE(a,b) _mm_store_ps(a,b)
#endif
void fill( float *data, int sz)
{
        int i;
        float init[4] = {1,2,3,4};
        __m128 d = _mm_loadu_ps(init);
        for(i=0;i<sz;i++)
                STORE(&data[i*4],d);
}
Для остальных случаев приведу cущественную часть, дабы не повторяться:
void copy_movaps( float *from, float *to, int sz){
        for(i=0;i<sz;i++)
                STORE(&to[i*4],_mm_load_ps(&from[i*4]));

void stream(float *data, int sz) {
        float init[4] = {1,2,3,4};
        __m128 c = _mm_loadu_ps(init);
        for(i=0;i<sz;i++){
                __m128 d = _mm_load_ps(&data[i*4]);
                d = _mm_add_ps(d,c);
                STORE(&data[i*4],d);
        }

Результаты

Вышеупомянутые сниппеты гонялись на 0.8-3.2Gb данных (в зависимости от машины) на четырех CPU: Core i7-AVX (@4.5Ghz), Core i7-920 (@3Ghz), Core2 Q9300 (@2.5) и Core2 T7500 (@2.2). Выборочная проверка показала, что для таких коротких сниппетов каких-то компиляторных взбрыков (как было у интела на более сложном коде) - нету.

Результаты сведены в таблицу. Там присутствуют и результаты для stream2/stream3 про которые подробно рассказано ниже.

 Производительность, Mbyte/sec
Core I7 2600K @4.5Ghz
DDR3 1800, 2chan
Core I7-920 @3Ghz
DDR3 1500, 3chan
Core2 Q93000 @2.5Ghz
DDR2 800, 2chan
Core2 T7500 @2.2Ghz
DDR2 667, 2chan?
fill-MOVAPS12206948426141920
fill-MOVNTPS227611512267323588
copy-MOVAPS8122596019461513
copy-MOVNTPS11673815728891970
stream-MOVAPS11808893625951920
stream-MOVNTPS10454653533431920
stream2-MOVAPS1041461492613-
stream2-MOVNTPS1008258363243-
stream3-MOVAPS893645632674-
stream3-MOVNTPS94074062537-

Жирным шрифтом в таблице выделены случаи, когда movntps-реализация получилась медленнее, чем movaps-реализация.

fill и copy

И для copy и для fill movntps очень полезен. До 2.5 раз быстрее (для fill) - это много.

Речь именно о больших объемах, готов допустить, что если результат влезает в L1-кэш, то разница меньше.

stream

С простой потоковой обработкой (добавление константы к значениям в памяти) ситуация сложнее: для Core2 movntps оказывается полезен (для более нового варианта) или, как минимум, не вреден (для старого мобильного варианта).

С Core i7 ситуация обратная: для нового i7-AVX есть небольшой вред (в районе 3.5%), а для старого i7-920 вред оказывается изрядным (25%).

Внимательный читатель спросит: как же так, ведь два дня назад в этом блоге писалось обратное: от movntps на старом Core2 был обнаружен большой вред.

Вред действительно есть, но только в случае сложной обработки:

stream2 и stream3

Stream3 - это уже знакомое нам преобразование цветов, написанное на SSE4.1:
void stream3(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);      // I1, m1
                __m128 t0 = _mm_dp_ps(d0,m1,0xff); // I1, m2
                __m128 t1 = _mm_dp_ps(d0,m2,0xff); // I1, m3
                r0 = _mm_blend_ps(r0,t0,2);
                r0 = _mm_blend_ps(r0,t1,4);
                STORE(&data[i*4],r0);
        }
}
И этот код действительно нереально (в 5 раз) тормозит на Core2 (movntps против movaps на записи), на Core i7-920 ведет себя как и прочие варианты stream (минус 12%) и вполне нормально ведет себя на Core i7-AVX (movntps-вариант на 5% быстрее).

Если удалить один blendps и один dpps, то получится stream2:

void stream1_movntps(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);      // I1, m1
                __m128 t0 = _mm_dp_ps(d0,m1,0xff); // I1, m2
                r0 = _mm_blend_ps(r0,t0,2);
                STORE(&data[i*4],r0);
        }
}
И этот сниппет ведет себя уже так же, как простой stream1: на Core2 movntps-версия заметно быстрее, чем movaps; на Core i7 - несколько медленнее.

У меня нет никаких разумных объяснений, отчего stream3 так тормозит на Core2. Как минимум, дело не в лишней dpps, потому что вариант на чистом SSE2 (по мотивам этого поста) тоже тормозит на Core2, если сохранять выход через movntps.

Мораль:

  • Польза от movntps может быть и преизрядная.
  • Но крайне желательно проверять наличие этой пользы, а то вдруг выяснится что есть вред.

Из прочего, меня сильно порадовал рост memory bandwidth на последних двух поколениях процессоров. В три раза, если считать с Q9300, за три с небольшим года с осени 2007 по январь 2011.