Как сказать gcc, что данные, на которые указывает указатель, всегда будут выровнены?
В моей программе (написанной на простом C) у меня есть структура, в которой хранятся данные, подготовленные для преобразования с помощью векторного (только AVX) двумерного быстрого преобразования Фурье. Структура выглядит так:
struct data {
double complex *data;
unsigned int width;
unsigned int height;
unsigned int stride;
};
Теперь мне нужно как можно быстрее загрузить данные из памяти. Насколько я знаю, существует не выровненная и выровненная загрузка в регистры ymm (инструкции vmovapd и vmovupd), и я хотел бы, чтобы программа использовала выровненную версию как более быструю.
Пока что я использую примерно одинаковую конструкцию для всех операций над массивом. Этот пример является частью программы, когда данные и фильтр уже преобразованы в частотную область, и фильтр применяется к данным по каждому элементу умножения.
union m256d {
__m256d reg;
double d[4];
};
struct data *data, *filter;
/* Load data and filter here, both have the same width, height and stride. */
unsigned int stride = data->stride;
for(unsigned int i = 0; i<data->height; i++) {
for(unsigned int j = 0; j<data->width; j+=4) {
union m256d a[2];
union m256d b[2];
union m256d r[2];
memcpy(a, &( data->data[i*stride+j]), 2*sizeof(*a));
memcpy(b, &(filter->data[i*stride+j]), 2*sizeof(*b));
r[0].reg = _mm256_mul_pd(a[0].reg, b[0].reg);
r[1].reg = _mm256_mul_pd(a[1].reg, b[1].reg);
memcpy(&(data->data[i*stride+j]), r, 2*sizeof(*r));
}
}
Как и ожидалось, вызовы memcpy оптимизированы. Однако после наблюдения gcc преобразует memcpy либо в две инструкции vmovupd, либо в набор команд movq, которые загружают данные в гарантированно выровненное место в стеке, а затем в две инструкции vmovapd, которые загружают их в регистры ymm. Это поведение зависит от того, определен ли прототип memcpy или нет (если он определен, то gcc использует movq и vmovapd).
Я могу гарантировать, что данные в памяти выровнены, но я не уверен, как сказать gcc, что он может просто использовать команды movapd для загрузки данных из памяти прямо в регистры ymm. Я сильно подозреваю, что gcc не знает, что данные, указанные &(data->data[i*stride+j])
всегда выровнены.
Есть ли вариант, как сказать gcc, что данные, на которые указывает указатель, всегда будут выровнены?
2 ответа
vmovupd
так же быстро, как vmovapd
когда данные фактически выровнены во время выполнения. Единственная разница в том, что vmovapd
ошибки, когда данные не выровнены. (См. Ссылки по оптимизации в вики-теге x86, в частности , PDF-файлы Agner Fog по оптимизации и микроархитектуре и руководство по оптимизации Intel.
У вас проблема только в том случае, если она когда-либо использует несколько инструкций вместо одной.
Поскольку вы используете встроенные функции Intel для _mm256_mul_pd
, используйте встроенные функции загрузки / хранения вместо memcpy! Смотрите вики- теги sse для руководства по встроенным функциям и многого другого.
// Hoist this outside the loop,
// mostly for readability; should optimize fine either way.
// Probably only aliasing-safe to use these pointers with _mm256_load/store (which alias anything)
// unless C allows `double*` to alias `double complex*`
const double *flat_filt = (const double*)filter->data;
double *flat_data = (double*)data->data;
for (...) {
//union m256d a[2];
//union m256d b[2];
//union m256d r[2];
//memcpy(a, &( data->data[i*stride+j]), 2*sizeof(*a));
__m256d a0 = _mm256_load_pd(0 + &flat_data[i*stride+j]);
__m256d a1 = _mm256_load_pd(4 + &flat_data[i*stride+j]);
//memcpy(b, &(filter->data[i*stride+j]), 2*sizeof(*b));
__m256d b0 = _mm256_load_pd(0 + &flat_filt[i*stride+j]);
__m256d b1 = _mm256_load_pd(4 + &flat_filt[i*stride+j]);
// +4 doubles = +32 bytes = 1 YMM vector = +2 double complex
__m256d r0 = _mm256_mul_pd(a0, b0);
__m256d r1 = _mm256_mul_pd(a1, b1);
// memcpy(&(data->data[i*stride+j]), r, 2*sizeof(*r));
_mm256_store_pd(0 + &flat_data[i*stride+j], r0);
_mm256_store_pd(4 + &flat_data[i*stride+j], r1);
}
Если бы вы хотели выровнять загрузку / хранилище, вы бы использовали _mm256_loadu_pd
/ storeu
,
Или вы могли бы просто бросить свой double complex*
к __m256d*
и разыменовывал это напрямую. В GCC это эквивалентно встроенной нагрузке. Но обычное соглашение - использовать встроенные функции загрузки / хранения.
Однако, чтобы ответить на вопрос заголовка, вы можете помочь gcc автоматически векторизовать, сообщив, когда указатель гарантированно будет выровнен:
data = __builtin_assume_aligned(data, 64);
В C++ вам нужно привести результат, но в C void*
свободно разливается
См. Как сказать GCC, что аргумент указателя всегда выровнен по двойному слову? и https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html.
Это, конечно, характерно для диалектов GNU C/C++ (clang, gcc, icc), не переносимых на MSVC или другие компиляторы, которые не поддерживают расширения GNU.
Пока что я использую примерно одинаковую конструкцию для всех операций над массивом.
Многократные циклы по массиву обычно хуже, чем делать как можно больше за один проход. Даже если в L1D все остаётся горячим, только дополнительные инструкции по загрузке и хранению являются узким местом по сравнению с выполнением большего, пока ваши данные находятся в регистрах.
Как указал Олаф, можно написать соответствующие функции загрузки и сохранения. Итак, теперь код хорошо переводится в две команды vmovapd при загрузке и две в vmovapd при сохранении.
static inline void mload(union m256d t[2], double complex *f)
{
t[0].reg = _mm256_load_pd((double *)f);
t[1].reg = _mm256_load_pd((double *)(f+2));
}
static inline void msave(union m256d f[2], double complex *t)
{
_mm256_store_pd((double *)t, f[0].reg);
_mm256_store_pd((double *)(t+2), f[1].reg);
}
unsigned int stride = data->stride;
for(unsigned int i = 0; i<data->height; i++) {
for(unsigned int j = 0; j<data->width; j+=4) {
union m256d a[2];
union m256d b[2];
union m256d r[2];
mload(a, &( data->data[i*stride+j]));
mload(b, &(filter->data[i*stride+j]));
r[0].reg = _mm256_mul_pd(a[0].reg, b[0].reg);
r[1].reg = _mm256_mul_pd(a[1].reg, b[1].reg);
msave(r, &(data->data[i*stride+j]));
}
}