Как сказать 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]));
    }
}
Другие вопросы по тегам