Про AVX2

Давненько я в этом блоге на ассемблере не писал.

Вот значит кусочек FRV, который накладывает выходную тоновую кривую. Точнее, два кусочка, один на старом добром SSE2 (которого там ровно одна команда, сконвертировать float-int), а второй - на новом модном AVX2, правда 128-битном, но зато с table lookup.

#ifndef USE_AVX
        uint32_t __declspec(align(16)) dpix[4];
        _mm_store_si128((__m128i *)dpix, _mm_cvttps_epi32(pixel));
        dest[col] = 0xff000000 | curve[dpix[2]] << 16 | curve[dpix[1]] << 8 | curve[dpix[0]];
#else
        pixel = _mm_max_ps(pixel, alpha);
        __m128i ldata = _mm_i32gather_epi32((const int*)curve32, _mm_cvttps_epi32(pixel), 4);
        __m128i packed = _mm_packus_epi16(_mm_packus_epi32(ldata, izero), izero);
        dest[col] = _mm_cvtsi128_si32(packed);
#endif

Значит комментарии:

  • pixel - пиксель в плавучке, диапазон данных 0..65535.f. В 4-м канале мусор (могут быть остатки от канала G2, к примеру). Сразу после color conversion.
  • curve,curve32 - тоновые кривые. unsigned char[0x10000] и unsigned int[0x10000]. На выходе диапазон 0..255
  • alpha - насыщает альфа-канал, там константа [64k,0,0,0]
  • izero - нули, константа.

В реальном коде весь AVX2-вариант выписан в одну строчку, на результат (генерируемый код) это не влияет.

В реальном же SSE2-коде (в смысле - в сгенерированном компилятором) никакой временной переменной dpix на стеке нету, все живет в XMM-регистре.

И что бы вы думали? AVX2-вариант изрядно медленнее. Раза в полтора, если брать эту конкретную строчку (она, впрочем, 75% всей функции по времени, load-store и умножения на матрицу - ничего не занимают).

Вот вам и новые технологии.

Я предполагаю (но доказать не могу, вывод VTune настолько хорошо - не читаю), что дело в размере кривой в памяти. 1-байтовая занимает 64к, 4-байтовая - соответственно. И весь выигрыш от lookup одной инструкцией (если он вообще есть) пожирается не-влезанием в кэш.

Надо попробовать сократить кривую разиков, к примеру, в 8.

Update: после сокращения кривой в 16 раз, там стал 12-битный вход, разница стала меньше, но все равно в пользу простого и человекопонятного кода без _mm_i32gather_epi32().

Comments

Gather выгоден в основном в случае когда много собираемых элементов находятся в одной cache line. Иначе load-ы быстрее. Да и когда много в одной кеш линии можно получить более быстрый код из load+shuffle если паттерн доступа известен заранее (icc умеет такое автоматически).

Ну вот у меня
а) table lookups
b) гистограммы (там, понятно, gather и дальше поэлементная запись обратно)

Паттерн зависит от данных конкретного пикселя, никак иначе.

Да, я знаю, что гистограммы - самое больное место у многих, но без них совсем никуда. И так огрубляю их сколько могу.

Если паттерн в table lookups непредсказуем, то насколько я знаю в HSW будет медленнее чем через загрузки (disclaimer: я в основном работаю с кодом состоящим из FMA а не из gather-ов :)). В предположении что рядом расположенные пиксели сильно отличатся часто не будут и локальность look-up-ов будет хот какая-то (не копенгаген насколько это правда), то возможно векторизация на полную длину вектора что-нибудь и даст. Но сильно не уверен.

Про гистограммы -- да, больное место.

Ну да, полная длина - это два пикселя, там поканальные значения будут близкие (но не одинаковые - фотонный шум), а 4-й компонент (альфа) - вовсе одинаковый.

Буду пробовать, не догоню так хоть согреюсь: в других местах должен получиться выигрыш от удвоения (но и код распухнет: пикселей не обязательно четное количество, придется хвостики подбирать)

http://blog.lexa.ru/2015/12/20/eshche_pro_avx2_i_vpgatherdd.html

ES: с полным вектором лучше, но "тупой скалярный код на C++" все равно лучше для гистограммы и существенно.

К слову, тут в http://www.agner.org/optimize/blog/read.php?i=415 обнаружилось чудесное: «I observed an interesting phenomenon when executing 256-bit vector instructions on the Skylake. There is a warm-up period of approximately 14 µs before it can execute 256-bit vector instructions at full speed. Apparently, the upper 128-bit half of the execution units and data buses is turned off in order to save power when it is not used. As soon as the processor sees a 256-bit instruction it starts to power up the upper half. It can still execute 256-bit instructions during the warm-up period, but it does so by using the lower 128-bit units twice for every 256-bit vector. The result is that the throughput for 256-bit vectors is 4-5 times slower during this warm-up period. If you know in advance that you will need to use 256-bit instructions soon, then you can start the warm-up process by placing a dummy 256-bit instruction at a strategic place in the code. My measurements showed that the upper half of the units is shut down again after 675 µs of inactivity.» И, судя по комментариям, похожее поведение есть и в SB-EP (а следовательно, потенциально и в HSW/BSW) (дальше по тексту ссылка на http://agner.org/optimize/blog/read.php?i=378#378 ).

Отличное чтение, только блин какое-то бесполезное.

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