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

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

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

Do hwvec1 и arr1 зависят от undefined behavior s?

Они нарушают строгие правила наложения имен? [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 ]

0 голосов
/ 31 августа 2018

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 иногда segfault на AMD64? )


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

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

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

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


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

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

0 голосов
/ 31 августа 2018

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

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

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

...