Как указать компилятору генерировать невыровненные нагрузки для __m128

У меня есть код, который работает со значениями __m128. Я использую встроенные функции SSE x86-64 для этих значений и обнаружил, что если значения не выровнены в памяти, происходит сбой. Это связано с тем, что мой компилятор (в данном случае clang) генерирует только выровненные инструкции загрузки.

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


Причина, по которой у меня невыровненные значения, в первую очередь, заключается в том, что я пытаюсь сэкономить память. У меня struct примерно такой:

#pragma pack(push, 4)
struct Foobar {
    __m128 a;
    __m128 b;
    int c;
};
#pragma pack(pop)

Затем я создаю массив этих структур. Второй элемент массива начинается с 36 байтов, что не кратно 16.

Я знаю, что могу переключиться на структуру представления массивов или удалить прагму упаковки (за счет увеличения размера структуры с 36 до 48 байтов); но я также знаю, что невыровненные нагрузки в наши дни не так уж дороги, и хотел бы сначала попробовать это.


Обновите, чтобы ответить на некоторые из комментариев ниже:

Мой реальный код был ближе к этому:

struct Vector4 {
    __m128 data;
    Vector4(__m128 v) : data(v) {}
};
struct Foobar {
    Vector4 a;
    Vector4 b;
    int c;
}

Затем у меня есть некоторые служебные функции, такие как:

inline Vector4 add( const Vector4& a, const Vector4 &b ) {
    return Vector4(_mm_add_ps(a.data, b.data));
}

inline Vector4 subtract( const Vector4& a, const Vector4& b ) {
    return Vector4(_mm_sub_ps(a.data, b.data));
}

// etc..

Я часто использую эти утилиты вместе. Поддельный пример:

Foobar myArray[1000];
myArray[i+1].b = sub(add(myArray[i].a, myArray[i].b), myArray[i+1].a);

Когда я смотрю на ответ "Z Bozon", мой код фактически изменился на:

struct Vector4 {
    float data[4];
};

inline Vector4 add( const Vector4& a, const Vector4 &b ) {
    Vector4 result;
    _mm_storeu_ps(result.data, _mm_add_ps(_mm_loadu_ps(a.data), _mm_loadu_ps(b.data)));
    return result;
}

Меня беспокоило то, что, когда служебные функции использовались в комбинации, как указано выше, сгенерированный код мог иметь избыточные инструкции загрузки / сохранения. Оказывается, это не было проблемой. Я протестировал свой компилятор (clang), и он все удалил. Я приму ответ З. Бозона.


person pauldoo    schedule 24.11.2015    source источник
comment
Не используйте __m128 в своей структуре. Используйте, например, float a[4] и явно выполняйте загрузку и сохранение с помощью _mm_loadu_ps и _mm_storeu_ps.   -  person Z boson    schedule 24.11.2015
comment
Похоже, OP не только использует явные встроенные функции, но и в некоторых случаях получает SIMD-код, сгенерированный clang из-за автоматической векторизации?   -  person Paul R    schedule 24.11.2015
comment
@PaulR, если это так, OP должен добавить эту информацию к своему вопросу.   -  person Z boson    schedule 24.11.2015
comment
Да, сложно сказать, но это моя интерпретация первого абзаца ОП. Надеюсь, он зайдет позже и уточнить.   -  person Paul R    schedule 24.11.2015
comment
Я думаю, что большинство компиляторов будут генерировать выровненные нагрузки для получения переменных __m128, потому что этот тип данных имеет 16-байтовое выравнивание, определенное на языке C. Это означает, что если вы специально не издеваетесь над своим компилятором, он должен убедиться, что значение типа __m128 правильно выровнено. Вы заставляете компилятор сделать его невыровненным, что может противоречить правилам. Я считаю, что использование _mm_storeu_ps, как предлагает Z-бозон, является единственным надежным решением в таком случае.   -  person stgatilov    schedule 24.11.2015
comment
Какая именно у вас проблема? Вы видите, что компилятор генерирует выровненные загрузки / хранилища для копирования-назначения? Если да, то, может быть, скопируйте свои структуры с помощью memcpy? Лучше проверьте asm, чтобы убедиться, что он не ужасен. И если вы воспользуетесь хорошим предложением @zboson использовать массивы с плавающей запятой вместо __m128, gcc может скопировать каждый элемент массива отдельно. (Давным-давно в gcc произошла регрессия, которая до сих пор не исправлена, afaik, что он копирует структуры поэтапно, а не с более широкими загрузками / хранилищами). В этом случае мы получаем 64b целочисленных копий для обоих: goo.gl/Lm6TGi   -  person Peter Cordes    schedule 25.11.2015
comment
@PeterCordes, интересно. Вчера я видел что-то подобное с GCC, и мне не понравилось то, что я увидел, поэтому я двинулся дальше. OP говорит, что его компилятор в этом случае - Clang. Clang копии с SSE.   -  person Z boson    schedule 25.11.2015
comment
Если я использую float a [4] и напишу свою собственную явную загрузку и сохранение, сможет ли компилятор удалить избыточные загрузки и сохранения, которые возникают после встраивания функций и т. Д.? В настоящее время я не пишу свои собственные загрузки и хранилища, а вместо этого напрямую использую арифметические встроенные функции. Например. _mm_max_ps (myArray [300] .a, myArray [301] .a), просто позволяя компилятору генерировать нагрузки.   -  person pauldoo    schedule 25.11.2015
comment
@pauldoo: встроенные функции загрузки / сохранения не заставляют компилятор фактически выдавать инструкцию movdqu/a или mova/ups. Думайте о них больше как о способе передачи информации о выравнивании компилятору. Однако вы можете проверить, делает ли ваш компилятор то, что вы ожидаете. clang полностью оптимизирует некоторые магазины / перезагрузки до float local_a[4], но gcc делает их все. goo.gl/Mxe7oR. В любом случае все компиляторы могут складывать выровненные нагрузки в другие инструкции SSE в качестве операндов памяти, если они сочтут, что это лучше всего. (С AVX невыровненные грузы тоже могут складываться.)   -  person Peter Cordes    schedule 25.11.2015


Ответы (4)


На мой взгляд, вы должны писать свои структуры данных, используя стандартные конструкции C ++ (из которых __m128i нет). Если вы хотите использовать встроенные функции, которые не являются стандартными C ++, вы «входите в мир SSE» через встроенные функции, такие как _mm_loadu_ps, и вы «покидаете мир SSE» обратно в стандартный C ++ с помощью встроенного компонента, такого как _mm_storeu_ps. Не полагайтесь на неявные загрузки и сохранения SSE. Я видел слишком много ошибок при выполнении этого SO.

В этом случае вам следует использовать

struct Foobar {
    float a[4];
    float b[4];
    int c;
};

тогда ты можешь сделать

Foobar foo[16];

В этом случае foo[1] не будет выровнен по 16 байтам, но если вы хотите использовать SSE и оставить стандартный C ++, сделайте

__m128 a4 = _mm_loadu_ps(foo[1].a);
__m128 b4 = _mm_loadu_ps(foo[1].b);
__m128 max = _mm_max_ps(a4,b4);
_mm_storeu_ps(array, max);

затем вернитесь к стандартному C ++.

Еще одна вещь, которую вы можете рассмотреть, это

struct Foobar {
    float a[16];
    float b[16];
    int c[4];
};

затем, чтобы получить массив из 16 исходной структуры, выполните

Foobar foo[4];

В этом случае, пока первый элемент выровнен, все остальные элементы выровнены.


Если вам нужны служебные функции, которые работают с регистрами SSE, не используйте явную или неявную загрузку / сохранение в служебной функции. Передайте константные ссылки на __m128 и верните __m128, если нужно.

//SSE utility function
static inline __m128 mulk_SSE(__m128 const &a, float k)
{
    return _mm_mul_ps(_mm_set1_ps(k),a);
}

//main function
void foo(float *x, float *y n) 
{
    for(int i=0; i<n; i+=4)
        __m128 t1 = _mm_loadu_ps(x[i]);
        __m128 t2 = mulk_SSE(x4,3.14159f);
        _mm_store_ps(&y[i], t2);
    }
}

Причина использования константной ссылки заключается в том, что MSVC не может передавать __m128 по значению. Без константной ссылки вы получите ошибку

ошибка C2719: формальный параметр с __declspec (align ('16 ')) не будет выровнен.

__m128 для MSVC в любом случае действительно союз.

typedef union __declspec(intrin_type) _CRT_ALIGN(16) __m128 {
     float               m128_f32[4];
     unsigned __int64    m128_u64[2];
     __int8              m128_i8[16];
     __int16             m128_i16[8];
     __int32             m128_i32[4];
     __int64             m128_i64[2];
     unsigned __int8     m128_u8[16];
     unsigned __int16    m128_u16[8];
     unsigned __int32    m128_u32[4];
 } __m128;

предположительно MSVC не должен загружать объединение, когда служебные функции SSE встроены.


Основываясь на последнем обновлении кода OP, вот что я бы посоветовал

#include <x86intrin.h>
struct Vector4 {
    __m128 data;
    Vector4() {
    }
    Vector4(__m128 const &v) {
        data = v;
    }
    Vector4 & load(float const *x) {
        data = _mm_loadu_ps(x);
        return *this;
    }
    void store(float *x) const {
        _mm_storeu_ps(x, data);
    }
    operator __m128() const {
        return data;
    }
};

static inline Vector4 operator + (Vector4 const & a, Vector4 const & b) {
    return _mm_add_ps(a, b);
}

static inline Vector4 operator - (Vector4 const & a, Vector4 const & b) {
    return _mm_sub_ps(a, b);
}

struct Foobar {
    float a[4];
    float b[4];
    int c;
};

int main(void)
{
    Foobar myArray[10];
    // note that myArray[0].a, myArray[0].b, and myArray[1].b should be      // initialized before doing the following 
    Vector4 a0 = Vector4().load(myArray[0].a);
    Vector4 b0 = Vector4().load(myArray[0].b);
    Vector4 a1 = Vector4().load(myArray[1].a);        
    (a0 + b0 - a1).store(myArray[1].b);
}

Этот код основан на идеях из библиотеки векторных классов Агнера Фога.

person Z boson    schedule 25.11.2015
comment
Спасибо за хорошо написанный ответ. Это может быть повторением вопроса, который я поставил в комментарии выше, но я хочу прояснить один момент. Если я использую предложенный вами подход и имею несколько служебных функций, каждая из которых входит и выходит из мира SSE таким образом. Когда я связываю эти функции (и давайте предположим, что они встроены), будет ли компилятор в целом достаточно умен, чтобы удалить избыточные операции storeu / loadu? Или мне придется писать предварительно объединенные служебные функции, которые последовательно выполняют несколько действий, входя в мир SSE и покидая его только один раз? - person pauldoo; 26.11.2015
comment
@pauldoo, не могли бы вы добавить пример того, что вы имеете в виду, в свой вопрос? - person Z boson; 26.11.2015
comment
@pauldoo, как только вы заполнили регистр SSE данными, вы можете передать их функции, например. __m128 foo(__m128 const &a). Используйте константную ссылку, чтобы MSVC оставался довольным. - person Z boson; 26.11.2015
comment
@pauldoo, я добавил к своему ответу текст, который, надеюсь, относится к вашему комментарию. - person Z boson; 26.11.2015
comment
Я добавил пояснение к своему первоначальному вопросу и принял ваш ответ. - person pauldoo; 26.11.2015
comment
@pauldoo, я добавил новое решение на основе вашего разъяснения. - person Z boson; 27.11.2015
comment
Либо используйте функцию для загрузки и хранения данных, либо правильно выровняйте буферы (было ли это выравнивание 16 байт? Но лучше использовать функции - person BЈовић; 29.10.2019

У Clang есть -fmax-type-align. Если вы установите -fmax-type-align=8, то выровненные по 16 байтов инструкции не будут сгенерированы.

person Giovanni Funchal    schedule 29.10.2019

Вы можете попробовать изменить свою структуру на:

#pragma pack(push, 4)
struct Foobar {
    int c;
    __m128 a;
    __m128 b;
};
#pragma pack(pop)

Конечно, он будет иметь такой же размер и теоретически должен заставить clang генерировать невыровненные загрузки / хранилища.


В качестве альтернативы вы можете использовать явные невыровненные загрузки / магазины, например. изменение:

v = _mm_max_ps(myArray[300].a, myArray[301].a)

to:

__m128i v1 = _mm_loadu_ps((float *)&myArray[300].a);
__m128i v2 = _mm_loadu_ps((float *)&myArray[301].a);
v = _mm_max_ps(v1, v2);
person Paul R    schedule 24.11.2015
comment
Для вашего альтернативного предложения OP может также использовать float a[4]. - person Z boson; 25.11.2015
comment
Верно - я просто добавлял иллюстрацию для текущего определения структуры, но было бы разумнее переключиться на массив с плавающей запятой, и это также избавило бы от уродливых приведений. - person Paul R; 25.11.2015

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

#pragma vector unaligned //for C/C++ 

CDEC$ vector unaligned ; for Fortran

Это в первую очередь предназначено для контроля компромиссов между «выровненными, но очищенными» и «не очищенными, но невыровненными». Дополнительные сведения см. На странице https://software.intel.com/en-us/articles/utilizing-full-vectors

Насколько мне известно, это работает только для компиляторов Intel. Компиляторы Intel также имеют внутренний переключатель компиляции -mP2OPT_vec_alignment = 6, чтобы сделать то же самое для всего модуля компиляции.

Я не проверял, можно ли его эффективно применить к реализациям, в которых встроенные функции / сборки используются вместе с OpenMP / Cilk.

person zam    schedule 25.11.2015