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

Армянское радио Нас спрашивают:

Как измениться производительность intrinsic варианта на Core-i7, если поменять
_mm_dp_ps на _mm256_dp_ps
_mm_blend_ps на _mm_blend256_ps

То-есть насколько вырастить производительность если мы совсем на AVX переедем и будет обрабатывать по 8 float за проход? А то слухи разные ходят... от 0% до 200% роста.

Отвечаем:

Если игнорировать возможную нечетность размера данных, то код получается таким:

ALIGN1(32) float matX2[3][8] ALIGN2(32) = {
        { 0.17f,0.22f,0.33f,0.44f,0.17f,0.22f,0.33f,0.44f},
        {0.55f,0.66f,0.77f,0.88f,0.55f,0.66f,0.77f,0.88f},
        {1.01f,1.02f,1.03f,1.04f,1.01f,1.02f,1.03f,1.04f}
} ;

void dotp_avx(float data[], int sz)
{
        int i;
        __m256 m0 = _mm256_load_ps(matX2[0]);
        __m256 m1 = _mm256_load_ps(matX2[1]);
        __m256 m2 = _mm256_load_ps(matX2[2]);

        for(i=0;i<sz/2;i++)
        {
                __m256 d0 = _mm256_load_ps(&data[i*8]);
                _mm_prefetch((const char*)&data[i*8+16],_MM_HINT_NTA);
                __m256 r0 = _mm256_dp_ps(d0,m0,0xf1);  
                __m256 t0 = _mm256_dp_ps(d0,m1,0xff);
                __m256 t1 = _mm256_dp_ps(d0,m2,0xff);
                r0 = _mm256_blend_ps(r0,t0,0x22);
                r0 = _mm256_blend_ps(r0,t1,0x44);
                _mm256_stream_ps(&data[i*8],r0);       
        }
}
Матрицу на которую множим - пришлось удвоить, естественно. Остальное все прозрачно, про _mm_prefetch напишу ниже, в результатах.

Да, для удобства родились такие вот макросы, используемые выше:

#if defined(_MSC_VER) || defined(__INTEL_COMPILER)
#define ALIGN1(v) __declspec(align(v))
#define ALIGN2(v)
#else /* GCC */
#define ALIGN1(v)
#define ALIGN2(v) __attribute__((aligned(v)))
#endif

Результаты

  • Без _mm_prefetch: 625 Mpix/sec
  • С _mm_prefetch: 637 Mpix/sec
Скорость практически одинакова (разница в пределах разброса отдельных прогонов) для всех компиляторов (Intel 12.0, MSVC 2010 SP1, gcc 4.6.2).

Другими словами, в 1.36 раза быстрее, чем было с SSE4.1 на Intel C++ и в 1.14 раза быстрее, чем для MSVC.

Comments

а разве они уже умеют 1 блоком делать операцию над 256 бит вектором за раз ?
Вроде ж писали, что регистры по 256, но каждую половинку по факту обрабатывает свой блок.

Ну так а мне, по счастью, и не надо 256 бит за раз, у меня скалярное умножение двух векторов длиной 4
И _mm256_dp_ps() делает именно это - умножает зараз две половинки.

На моём i7 2670QM (2GHz) этот код выдаёт 475 MPix/sec если данные в L2, 450 MPix/sec если данные в L3, и 310 MPix/sec если данные в памяти

SECTION .rdata
align 32
table0 dd 0.44, 0.33, 0.22, 0.11, 0.44, 0.33, 0.22, 0.11
table1 dd 0.88, 0.77, 0.66, 0.55, 0.88, 0.77, 0.66, 0.55
table2 dd 1.04, 1.03, 1.02, 1.01, 1.04, 1.03, 1.02, 1.01

SECTION .text

global convert_pixels

; extern "C" void convert_pixels(const float* source, float* destination, size_t length)
convert_pixels:
; rcx - source
; rdx - destination
; r8 - length

vzeroupper
align 32
.main_processing_loop:
vmovaps ymm0, [rcx]
vmovaps ymm1, [rcx + 32]
vmovaps ymm2, [rcx + 64]
vmovaps ymm3, [rcx + 96]

vmulps ymm4, ymm0, [table0]
vmulps ymm5, ymm0, [table1]
vmulps ymm6, ymm0, [table2]
vmulps ymm7, ymm1, [table0]
vmulps ymm8, ymm1, [table1]
vmulps ymm9, ymm1, [table2]
vmulps ymm10, ymm2, [table0]
vmulps ymm11, ymm2, [table1]
vmulps ymm12, ymm2, [table2]
vmulps ymm13, ymm3, [table0]
vmulps ymm14, ymm3, [table1]
vmulps ymm15, ymm3, [table2]

vhaddps ymm4, ymm4, ymm5
vhaddps ymm6, ymm6, ymm7
vhaddps ymm8, ymm8, ymm9
vhaddps ymm10, ymm10, ymm11
vhaddps ymm12, ymm12, ymm13
vhaddps ymm14, ymm14, ymm15

vhaddps ymm4, ymm4, ymm6
vhaddps ymm8, ymm8, ymm10
vhaddps ymm12, ymm12, ymm14

vmovaps [rdx], ymm4
vmovaps [rdx + 32], ymm8
vmovaps [rdx + 64], ymm12
sub rcx, -128
add rdx, 96
sub r8, 32
jnz .main_processing_loop
vzeroupper

ret

Попробуйте погонять вот этот код (компилировать Nasm'ом)

; Processing speed on Core i7 2630QM (2GHz, Signle-Channel DDR3-1333 (PC3-10700))
; * Data in memory: 8400 MB/s (300 MPix/s)
; * Data in L3: 11300 MB/s (400 MPix/s)
; * Data in L2: 11500 MB/s (410 MPix/s)

SECTION .rdata align(32)
c1c0 dd 0.88, 0.77, 0.66, 0.55, 0.44, 0.33, 0.22, 0.11
c2c1 dd 1.04, 1.03, 1.02, 1.01, 0.88, 0.77, 0.66, 0.55
c0c2 dd 0.44, 0.33, 0.22, 0.11, 1.04, 1.03, 1.02, 1.01

SECTION .text

global convert_pixels

; extern "C" void convert_pixels(const float* source, float* destination, size_t length)
convert_pixels:
; rcx - source
; rdx - destination
; r8 - length

vzeroupper
align 32
.main_processing_loop:
prefetchnta [rcx + 1408]

vmovaps ymm1, [rcx]
vmovaps ymm12, [rcx + 32]
vmovaps ymm13, [rcx + 64]
vmovaps ymm11, [rcx + 96]

vperm2f128 ymm2, ymm1, ymm12, 00100000b
vperm2f128 ymm3, ymm1, ymm12, 00100001b
vperm2f128 ymm4, ymm12, ymm13, 00100000b
vperm2f128 ymm6, ymm12, ymm13, 00100001b
vperm2f128 ymm7, ymm12, ymm13, 00110001b
vperm2f128 ymm8, ymm13, ymm11, 00100001b
vperm2f128 ymm9, ymm13, ymm11, 00110001b

vmulps ymm0, ymm1, [c1c0]
vmulps ymm1, ymm1, [c2c1]
vmulps ymm2, ymm2, [c0c2]
vmulps ymm3, ymm3, [c1c0]
vmulps ymm4, ymm4, [c0c2]
vmulps ymm5, ymm6, [c1c0]
vmulps ymm6, ymm6, [c2c1]
vmulps ymm7, ymm7, [c0c2]
vmulps ymm8, ymm8, [c2c1]
vmulps ymm9, ymm9, [c0c2]
vmulps ymm10, ymm11, [c1c0]
vmulps ymm11, ymm11, [c2c1]

vhaddps ymm0, ymm0, ymm1
vhaddps ymm2, ymm2, ymm3
vhaddps ymm4, ymm4, ymm5
vhaddps ymm6, ymm6, ymm7
vhaddps ymm8, ymm8, ymm9
vhaddps ymm10, ymm10, ymm11

vhaddps ymm0, ymm0, ymm2
vhaddps ymm4, ymm4, ymm6
vhaddps ymm8, ymm8, ymm10

vmovntps [rdx], ymm0
vmovntps [rdx + 32], ymm4
vmovntps [rdx + 64], ymm8
sub rcx, -128
add rdx, 96
sub r8, 32
jnz .main_processing_loop
vzeroupper

ret

Попробую, но пока не понимаю что он делает, тем паче что передача параметров через регистр с инфраструктурой C++ как-то плохо совместима, ну да придумаю что-нибудь.

Calling convention стандартная для Windows x64. В ней первые четыре параметра передаются в регистрах

Я что про это хочу сказать (пройдя в отладчике и поняв)
1) Идея отличная, работать должно быстро.
Но
2) То что мы 16 входных значений упаковали в 12 выходных - это для каких-то применений хорошо, а для последующей обработки в FP - категорически неудобно. Поэтому в моих реализациях 4-й компонент выхода обнулялся и это было спецально.

3) Конкретно в этом коде кажется есть ошибка (проверил дважды, вроде в _mm я все правильно перенес) от которой в ymm4 на выходе неправильно.

2) Сделать код, который будет выдавать 16 выходных значений гораздо проще, но я оптимизировал код из этого поста, который выдаёт три компоненты
3) Что-то я не вижу ошибки. Комментарии к коду добавил здесь

Не, тот код меняет 3 компоненты, а 4-ю просто оставляет как есть. В том коде просто нету внешнего цикла, который img+=4
А вот тут он есть, ну да не суть.

Что касается ошибки, то я просто накормил повторяющимися входными данными (1,2,3,4) и ожидал увидеть повторяющиеся выходные, а в ymm4 было другое. Возможно, я просто неаккуратно перенес (хотя проверил).

Хрен с ним, идея что можно горизонтально складывать для скалярного произведения - понятна, а остальное неважно.

Ок, вот новая версия для 4-х компонентных пискелей на выходе. Теперь банановый ещё быстрее

Ага, пощупаю.

А действительно работать в два потока "с начала до середины" и "с середины до конца" - быстрее?

На моём ноуте быстрее, хотя ненамного (8700 MB/s vs 8500 MB/s) и я не уверен, что разница статистически значима. В общем случае это зависит от множества параметров (сколько массивов данных читает/пишет программа, сколько буферов загрузки и записи есть в процессоре, сколько cache misses может одновременно обрабатывать контроллер кэша, сколько открытых DRAM страниц может держать контроллер памяти, и т.д.). Я как раз делаю ресёрч на эту тему

Ого!

1032Mpix/sec (16.5gb/sec)
Это не развернутый вариант, а по 8 значений (один регистр) за раз, in place (пишем откуда читали) и без префетча. Для сравнения с другими.

И не ассемблер, а intrinsics, хотя разницы быть не должно.

Не, я в первый раз неправильно померял (в инварианте цикла ошибся).

725Mpix/sec. Быстрее чем все что раньше, но и только.