Цикл векторизации / оптимизации с доступом к невыровненным данным для широких регистров (в частности, Xeon Phi)

Это мой первый опыт задавания вопросов сообществу Stackoverflow. Извините, если мой вопрос не соответствует стилю / размеру форума — улучшится с опытом.

Я пытаюсь векторизовать цикл в C ++, используя Intel Compiler 14.0.1, чтобы лучше использовать широкие 512-битные регистры для оптимизации скорости на Intel Xeon Phi.
(вдохновленный https://software.intel.com/en-us/articles/data-alignment-to-assist-vectorization) и многочисленные ссылки в Google, что выравнивание данных гораздо важнее на Xeon Phi, чем на современных процессорах Xeon, где это все еще важно (одна из них в хорошем обзоре https://indico.cern.ch/event/238763/material/slides/6.pdf на с.18).

Этот вопрос чем-то похож на доступ к памяти без выравнивания, но охватывает более простой / более распространенный пример и, надеюсь, имеет более четкий ответ.

Пример кода:

#include <malloc.h>void func(float *const y, float  *const x, const int & N, const float & a0, const float & a1, const float & a2, const float & a3)
{
__assume(N%16 == 0); // aim is to let compiler know that there is no residual loop (not sure if it works as expected, though)

int i;
#pragma simd // to assume no vector dependencies
#pragma loop count min=16, avg=80, max=2048 // to let compiler know for which cases to optimize (not sure if it is beneficial)
//#pragma vector aligned // to let compiler know that all the arrays are aligned... but not in this case
for (i = 0; i < N; i++)
{
y[i] = fmax(x[i + 1] * a0 + x[i] * a1, x[i] * a2 + a3);
}

}

int main{

...
//y and x are _mm_malloced with 64 byte alignment, e.g.

float * y = (float *)_aligned_malloc(int_sizeBytes_x_or_y + 64, 64); //+64 for padding to enable vectorisation without using mask on the residual loop
float * x = (float *)_aligned_malloc(int_sizeBytes_x_or_y + 64, 64);
...
//M = 160 to 2048, more often 160 (a multiple of 16 - floats per register)
for (int k = 0; k < M; k++)
{
...
//int N = ceil(k / 16.0) * 16; // to have no residual loop, not sure if beneficial
...func(y, x, N, a0, a1, a2, a3);...
}
...
_aligned_free(x);
_aligned_free(y);
}

Функция func () вызывается в теле 150-2000 раз, повторно используя предварительно выделенное пространство для x и y (чтобы избежать постоянных выделений памяти, которые, по-видимому, относительно более трудоемки на Phi, чем на обычном Xeon). Тело повторяется миллионы раз на каждом ядре.

Проблема в том, что x [i] и x [i + 1] по своей природе не выровнены для 512-битного векторного механизма, что делает векторизацию неоптимальной из-за неправильно выровненного доступа к памяти для части x [i + 1].

Будут ли какие-то преимущества с точки зрения скорости для предварительного выделения 64-байтового выравниваемого _x один раз перед циклом k ++, делая memcpy для заполнения предварительно выделенной памяти переданными значениями x на каждой итерации цикла k ++? (эквивалент
for (int j=0; j<N; j++) _x[0]=x[i+1]; with memcpy) так, чтобы выровненный вектор #pragma мог использоваться внутри func () с y[i] = fmax(_x[i] * a0 + x[i] * a1, x[i] * a2 + a3);?

Может быть, есть какой-то хороший подход для эффективного решения этой довольно распространенной стандартной проблемы, чтобы наилучшим образом использовать векторный движок?

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

4

Решение

Даже в этом случае полезно сообщить компилятору, что массивы выровнены. Как в:
__assume_aligned (х, 64)
__assume_aligned (у, 64)

Что касается __assume (N% 16 == 0), это иногда может помочь, но вы увидите, что это чаще всего используется в кодах, которые имеют внутренний и внешний цикл. Стоимость остаточного цикла, который генерируется, когда N% 16 не равно 0, невелика, если вы нажмете его только один раз. Однако в этом случае вы вызываете функцию повторно. Так что это может помочь для больших значений М.

Не было бы хорошей идеей выделить второй массив и заполнить его значениями, начинающимися с x [1]. Memcpy слишком дорогой по сравнению со слегка выровненным доступом к памяти.

Вы можете попробовать переписать свой код, чтобы использовать встроенную функцию _mm512_alignr_epi32. Я пытался найти хороший пример, на который можно было бы указать, но пока не нашел. Но использование _mm512_alignr_epi32 может не принести вам большой пользы в этом случае, когда вы используете только 2 вектора.

3

Другие решения


По вопросам рекламы [email protected]