Является ли `reinterpret_cast`ing между указателем аппаратного вектора и соответствующим типом неопределенным поведением?

Законно ли делать такие вещи?

constexpr size_t _m256_float_step_sz = sizeof(__m256) / sizeof(float);
alignas(__m256) float stack_store[100 * _m256_float_step_sz ]{};
__m256& hwvec1 = *reinterpret_cast<__m256*>(&stack_store[0 * _m256_float_step_sz]);

using arr_t = float[_m256_float_step_sz];
arr_t& arr1 = *reinterpret_cast<float(*)[_m256_float_step_sz]>(&hwvec1);

Делать hwvec1 а также arr1 зависит от undefined behaviors?

Они нарушают строгие правила наложения имен? [basic.lval] / 11

Или существует только один определенный способ присваивания:

__m256 hwvec2 = _mm256_load_ps(&stack_store[0 * _m256_float_step_sz]);
_mm256_store_ps(&stack_store[1 * _m256_float_step_sz], hwvec2);

godbolt

2 ответа

Решение

ISO C++ не определяет __m256, поэтому нам нужно посмотреть, что определяет их поведение в реализациях, которые их поддерживают.

Внутренние особенности Intel определяют векторные указатели, такие как __m256* как разрешено псевдонимом чего-либо еще, так же, как ISO C++ определяет char* как разрешено псевдоним.

Так что да, безопасно разыменовать __m256* вместо использования _mm256_load_ps() встроенная нагрузка

Но особенно для float/double, часто проще использовать встроенные функции, потому что они заботятся о кастинге из float*, тоже. Для целых чисел встроенные функции загрузки / хранения AVX512 определяются как void*, но перед этим нужно дополнительное (__m256i*) что просто много беспорядка.


В gcc это реализуется путем определения __m256 с may_alias атрибут: из gcc7.3 avxintrin.h (один из заголовков, которые <immintrin.h> включает в себя):

/* The Intel API is flexible enough that we must allow aliasing with other
   vector types, and their scalar components.  */
typedef float __m256 __attribute__ ((__vector_size__ (32),
                                     __may_alias__));
typedef long long __m256i __attribute__ ((__vector_size__ (32),
                                          __may_alias__));
typedef double __m256d __attribute__ ((__vector_size__ (32),
                                       __may_alias__));

/* Unaligned version of the same types.  */
typedef float __m256_u __attribute__ ((__vector_size__ (32),
                                       __may_alias__,
                                       __aligned__ (1)));
typedef long long __m256i_u __attribute__ ((__vector_size__ (32),
                                            __may_alias__,
                                            __aligned__ (1)));
typedef double __m256d_u __attribute__ ((__vector_size__ (32),
                                         __may_alias__,
                                         __aligned__ (1)));

(Если вам интересно, вот почему разыменование __m256* как _mm256_store_ps не storeu.)

GNU C нативные векторы без may_alias разрешено псевдоним их скалярного типа, например, даже без may_alias Вы могли бы безопасно бросить между float* и гипотетический v8sf тип. Но may_alias делает его безопасным для загрузки из массива int[], char[] или что угодно.

Я говорю о том, как GCC реализует встроенные функции Intel только потому, что это то, с чем я знаком. Я слышал от разработчиков gcc, что они выбрали эту реализацию, потому что она требовалась для совместимости с Intel.


Другие особенности поведения Intel должны быть определены

Использование API Intel для _mm_storeu_si128( (__m128i*)&arr[i], vec); требует от вас создания потенциально не выровненных указателей, которые могли бы привести к ошибкам, если вы защитите их. А также _mm_storeu_ps для местоположения, которое не выровнено по 4 байта, требует создания выровненного float*,

Просто создавать невыровненные указатели или указатели вне объекта - это UB в ISO C++, даже если вы не разыменовываете их. Я предполагаю, что это позволяет реализации на экзотическом оборудовании, которые выполняют некоторые виды проверок указателей при их создании (возможно, вместо разыменования) или, возможно, не могут хранить младшие биты указателей. (Я понятия не имею, существует ли какое-либо конкретное оборудование, где возможен более эффективный код из-за этого UB.)

Но реализации, которые поддерживают встроенные функции Intel, должны определять поведение, по крайней мере, для __m* типы и float* / double*, Это тривиально для компиляторов, ориентированных на любой обычный современный процессор, включая x86 с плоской моделью памяти (без сегментации); указатели в asm - это просто целые числа, которые хранятся в тех же регистрах, что и данные. (У m68k есть адреса против регистров данных, но он никогда не отказывается от сохранения битовых комбинаций, которые не являются действительными адресами в регистрах A, если вы не разыграете их.)


Идем другим путем: доступ к элементу вектора.

Обратите внимание, что may_alias , словно char* Правило наложения имен действует только в одном направлении: безопасное использование не гарантируется. int32_t* читать __m256, Это может быть даже не безопасно использовать float* читать __m256, Так же, как это не безопасно char buf[1024];int *p = (int*)buf;,

Чтение / запись через char* может псевдоним что угодно, но когда у вас есть char объект, строгое псевдонимы делает UB читать его через другие типы. (Я не уверен, что основные реализации на x86 определяют это поведение, но вам не нужно полагаться на него, потому что они оптимизируют memcpy 4 байта в int32_t, Вы можете и должны использовать memcpy выразить невыровненную нагрузку от char[] буфер, потому что автоматическая векторизация с более широким типом допускает 2-байтовое выравнивание для int16_t* и создайте код, который потерпит неудачу, если это не так: почему при выравнивании доступа к памяти mmap иногда возникает ошибка на AMD64?)


Чтобы вставить / извлечь векторные элементы, используйте встроенные функции shuffle, SSE2 _mm_insert_epi16 / _mm_extract_epi16 или SSE4.1 вставить / _mm_extract_epi8/32/64, Для float нет встроенных / извлекаемых встроенных функций, которые вы должны использовать со скаляром float,

Или сохранить в массиве и прочитать массив. ( выведите переменную __m128i). Это на самом деле оптимизировать для извлечения векторных инструкций.

Синтаксис вектора GNU C обеспечивает [] оператор для векторов, как __m256 v = ...;v[3] = 1.25;, MSVC определяет векторные типы как объединение с .m128_f32[] член для доступа к элементу.

Существуют библиотеки-оболочки, такие как Vector Class Library от Agner Fog (лицензированная по лицензии GPL), которые предоставляют переносимые operator[] перегрузки для их векторных типов и оператор + / - / * / << и так далее. Это довольно хорошо, особенно для целочисленных типов, где наличие разных типов для разных значений ширины элемента делает v1 + v2 работать с нужным размером. (Синтаксис собственного вектора GNU C делает это для векторов с плавающей запятой / двойных и определяет __m128i как вектор со знаком int64_t, но MSVC не предоставляет операторов на базе __m128 типов.)


Вы также можете использовать объединение типов между вектором и массивом некоторого типа, что безопасно в ISO C99 и в GNU C++, но не в ISO C++. Я думаю, что это официально безопасно и в MSVC, потому что я думаю, как они определяют __m128 как нормальный союз.

Однако нет гарантии, что вы получите эффективный код от любого из этих методов доступа к элементам. Не используйте внутренние внутренние циклы, и посмотрите на полученный asm, если производительность имеет значение.

[edit: для downvoter, см. https://stackru.com/questions/tagged/language-lawyer. Этот ответ действителен для любого стандарта ISO C++ от C++98 до текущего проекта. Обычно предполагается, что базовые концепции, такие как неопределенное поведение, не нуждаются в подробном объяснении, но см. http://eel.is/c++draft/defns.undefined и различные вопросы по SO].

Это уже начинает быть неопределенным поведением из-за __m256 не является стандартным типом и не является допустимым именем для пользовательских типов.

Реализации могут, конечно, добавить конкретные дополнительные гарантии, но Undefined Behavior означает по отношению к ISO C++.

Другие вопросы по тегам