Как реализовать "mm storeu epi64" без проблем с алиасингом?


(Примечание: хотя этот вопрос касается "хранилища", случай "загрузки" имеет те же проблемы и идеально симметричен.)

Внутренние компоненты SSE обеспечивают _mm_storeu_pd функция со следующей сигнатурой:

void _mm_storeu_pd (double *p, __m128d a);
Поэтому, если у меня есть вектор из двух двойников, и я хочу сохранить его в массиве из двух двойников, я могу просто использовать эту внутреннюю функцию.

Однако мой вектор-это не два двойника; это два 64-битных целых числа, и я хочу сохранить его в массиве из двух 64-битных чисел. целое число. То есть мне нужна функция со следующей сигнатурой:

void _mm_storeu_epi64 (int64_t *p, __m128i a);
Но внутренние компоненты не обеспечивают такой функции. Самое близкое, что у них есть-это _mm_storeu_si128:
void _mm_storeu_si128 (__m128i *p, __m128i a);
Проблема в том, что эта функция принимает указатель на __m128i, в то время как мой массив является массивом int64_t. Запись в объект через неправильный тип указателя является нарушениемстрогого псевдонимирования и определенно неопределенным поведением. Я обеспокоен тем, что мой компилятор, сейчас или в будущем, изменит порядок или иначе оптимизировать магазин, тем самым нарушая мою программу странным образом.

Чтобы было понятно, что я хочу, это функция, которую я могу вызвать следующим образом:

__m128i v = _mm_set_epi64x(2,1);
int64_t ra[2];
_mm_storeu_epi64(&ra[0], v); // does not exist, so I want to implement it
Вот шесть попыток создать такую функцию.

Попытка №1

void _mm_storeu_epi64(int64_t *p, __m128i a) {
    _mm_storeu_si128(reinterpret_cast<__m128i *>(p), a);
}

Похоже, что у этого есть проблема строгого сглаживания, о которой я беспокоюсь.

Попытка №2

void _mm_storeu_epi64(int64_t *p, __m128i a) {
    _mm_storeu_si128(static_cast<__m128i *>(static_cast<void *>(p)), a);
}

Возможно, лучше вообще, но я не думаю, что это имеет какое-либо значение в данном случае.

Попытка #3

void _mm_storeu_epi64(int64_t *p, __m128i a) {
    union TypePun {
        int64_t a[2];
        __m128i v;
     };
    TypePun *p_u = reinterpret_cast<TypePun *>(p);
    p_u->v = a;
}

Это создает неверный код на моем компиляторе (GCC 4.9.0), который выдает выровненную movaps инструкцию вместо выровненной movups. (Союз выровнен, поэтому reinterpret_cast хитростью GCC предполагает, что p_u тоже выровнен.)

Попытка №4

void _mm_storeu_epi64(int64_t *p, __m128i a) {
    union TypePun {
        int64_t a[2];
        __m128i v;
     };
    TypePun *p_u = reinterpret_cast<TypePun *>(p);
    _mm_storeu_si128(&p_u->v, a);
}

Это, кажется, испускает код, который я хочу. Трюк" Type-punning via union", хотя технически не определен в C++, является широко поддерживаемым. Но является ли этот пример - где я передаю указатель на элемент Союза, а не доступ через сам Союз-действительно ли допустимый способ использовать союз для каламбура типа?

Попытка №5

void _mm_storeu_epi64(int64_t *p, __m128i a) {
    p[0] = _mm_extract_epi64(a, 0);
    p[1] = _mm_extract_epi64(a, 1);
}
Это работает и совершенно верно,но он выдает две инструкции вместо одной.

Попытка №6

void _mm_storeu_epi64(int64_t *p, __m128i a) {
    std::memcpy(p, &a, sizeof(a));
}
Это работает и совершенно справедливо... Я думаю. Но он выдает откровенно ужасный код на мою систему. GCC разливает a в выровненный слот стека через выровненное хранилище, затем вручную перемещает слова компонента в место назначения. (На самом деле он разливает его дважды, по одному для каждого компонента. Очень странный.)

...

Есть ли способ написать эту функцию, которая (А) будет генерировать оптимальный код на типичном современном компиляторе и (Б) будет иметь минимальный риск столкнуться со строгим алиасингом?

1 15

1 ответ:

SSE intrinsics - это один из тех нишевых угловых случаев, когда вам нужно немного подтолкнуть правила.

Поскольку эти встроенные компоненты являются расширениями компилятора (несколько стандартизированными Intel), они уже находятся вне спецификации стандартов языка C и C++. Таким образом, это несколько саморазрушительно, чтобы попытаться быть "стандартно совместимым", используя функцию, которая явно не является таковой.

Несмотря на то, что встроенные библиотеки SSE пытаются действовать как обычные сторонние библиотеки, ниже, все они специально обрабатываются компилятором.


Намерение:

Внутренние свойства SSE, вероятно, были разработаны с самого начала, чтобы позволить сглаживание между векторным и скалярным типами-поскольку вектор на самом деле является просто совокупностью скалярного типа. Но тот, кто проектировал внутренние компоненты SSE, вероятно, не был языковым педантом.
(в этом нет ничего удивительного. Программисты жесткого ядра с низким уровнем производительности и энтузиасты языкового юриспруденции, как правило, очень разные группы людей, которые не всегда ладят друг с другом.)

Мы можем видеть доказательства этого в свойствах load/store:

  • __m128i _mm_stream_load_si128(__m128i* mem_addr) - внутренняя нагрузка, которая принимает неконстантный указатель?
  • void _mm_storeu_pd(double* mem_addr, __m128d a) - Что делать, если я хочу хранить в __m128i*?
Проблемы строгого сглаживания являются прямым результатом этих плохих прототипов.

Начиная с AVX512, все внутренние компоненты были преобразованы в void*, чтобы решить эту проблему. задача:

  • __m512d _mm512_load_pd(void const* mem_addr)
  • void _mm512_store_epi64 (void* mem_addr, __m512i a)

Особенности Компилятора:

  • Visual Studio определяет каждый из типов SSE / AVX как объединение скалярных типов. Это само по себе позволяет строгое сглаживание. Кроме того, Visual Studio не использует строгое сглаживание, поэтому вопрос спорный:

  • Компилятор Intel никогда не подводил меня со всеми видами псевдонимов. Он, вероятно, не делает строгого сглаживания, хотя я никогда не находил ни одного надежный источник для этого.

  • GCC действительно выполняет строгое сглаживание, но, по моему опыту, не через границы функций. Он никогда не подводил меня, чтобы привести указатели, которые передаются в (на любом типе). ССЗ также объявляет типы ЕГЭ - __may_alias__, таким образом, явно позволяя ему псевдоним прочая.


Моя Рекомендация:

  • для параметров функции, которые имеют неправильный тип указателя, просто приведите его.
  • для переменных, объявленных и псевдоним в стеке, используйте объединение. Это объединение уже будет выровнено, так что вы можете читать/писать в них напрямую, без встроенных элементов. (Но имейте в виду проблемы пересылки хранилища, которые возникают при чередовании векторных/скалярных обращений.)
  • Если вам нужно получить доступ к вектору как в целом, так и по его скалярным компонентам, рассмотрите возможность использования Insert/extract intrinsics вместо aliasing.
  • при использовании GCC включите -Wall или -Wstrict-aliasing. Он расскажет вам о нарушениях строгого сглаживания.