Как реализовать "_mm_storeu_epi64" без проблем с псевдонимом?

(Примечание. Хотя этот вопрос касается "store", случай "load" имеет те же проблемы и является абсолютно симметричным.)

Внутренние функции 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. Запись на объект с помощью указателя неправильного типа является нарушением строгой псевдонимы и, безусловно, является undefined. Я обеспокоен тем, что мой компилятор, теперь или в будущем, изменит порядок или иным образом оптимизирует работу магазина, тем самым нарушая мою программу странными способами.

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

__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);
}

Кажется, что я испускаю код, который я хочу. технически undefined в С++, широко поддерживается, Но есть ли этот пример - где я передаю указатель на элемент союза, а не через сам союз, действительно ли можно использовать союз для типа-punning?

Попытка # 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

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

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

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


Цель:

Сценарии SSE, вероятно, были спроектированы с самого начала, чтобы разрешить сглаживание между векторным и скалярным типами - поскольку вектор действительно является всего лишь совокупностью скалярного типа.

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

Мы можем видеть доказательства этого в свойствах загрузки/хранения:

  • __m128i _mm_stream_load_si128(__m128i* mem_addr) - Внутренняя нагрузка, которая принимает указатель не const const?
  • 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 делает строгое сглаживание, но по моему опыту, а не через границы функций. Мне никогда не приходилось бросать указатели, которые передаются (на любом типе). GCC также объявляет типы SSE как __may_alias__, тем самым явно позволяя ему использовать другие типы.


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

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