硬件向量指针和相应类型之间的“ reinterpret_cast”运算是否是未定义的行为?

时间:2018-08-31 09:39:47

标签: c++ x86 language-lawyer intrinsics type-punning

做这样的事情合法吗?

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

hwvec1arr1是否取决于undefined behavior

它们是否违反严格的别名规则? [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 :(得分:2)

ISO C ++没有定义__m256,因此我们需要查看在支持它们的实现上定义的行为。

英特尔内部函数将__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的原因。)

不带may_alias

GNU C本机向量可以别名化其标量类型,例如即使没有may_alias,您也可以安全地在float*和假设的v8sf类型之间进行转换。但是may_alias可以安全地从int[]char[]或其他任何数组中加载。

我说的是GCC如何实现Intel的内在函数,因为那是我所熟悉的。我从gcc开发人员那里听说,他们选择该实现是因为与Intel兼容是必需的。


需要定义英特尔内部函数的其他行为

使用_mm_storeu_si128( (__m128i*)&arr[i], vec);的英特尔API要求您创建可能未对齐的指针,如果您引用它们会出错。并且_mm_storeu_ps到未对齐4字节的位置要求创建未对齐的float*

即使没有取消引用,只是创建未对齐的指针或对象外部的指针都是ISO C ++中的UB。我猜想这允许对奇异的实现硬件,它们在创建指针时可能会对指针进行某种检查(可能是在取消引用时进行检查),或者可能无法存储低位指针。 (我不知道是否存在任何特定的硬件,因为有了此UB,可以使用更高效的代码。)

但是支持Intel内在函数的实现必须至少对__m*类型和float* / double*定义行为。对于以任何普通现代CPU为目标的编译器而言,这都是微不足道的,包括具有平面内存模型(无分段)的x86。 asm中的指针只是与数据保存在同一寄存器中的整数。 (m68k具有地址和数据寄存器,但是只要您不对它们进行解引用,就可以将不是有效地址的位模式保留在A寄存器中,这从不会出错。)


走另一条路:向量的元素访问。

请注意,may_alias就像char*别名规则一样,只是一种方式保证使用{ {1}}读取int32_t*。使用__m256来读取float*甚至可能都不安全。就像做__m256 char buf[1024];并不安全一样。

通过int *p = (int*)buf;进行读/写操作可以为任何别名,但是当您拥有char* object 时,严格混叠确实使UB可以通过其他类型读取它。 (我不确定x86上的主要实现是否确实定义了这种行为,但是您不需要依赖它,因为它们将4个字节的char最佳化为memcpy。您可以并且应该使用int32_t来表示来自memcpy缓冲区的未对齐负载,因为允许使用更大类型的自动矢量化来假设char[]的2字节对齐,并且使代码失败,如果不是:Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?


要插入/提取矢量元素,请使用随机内在函数,SSE2 int16_t* / _mm_insert_epi16或SSE4.1 insert / _mm_extract_epi16。对于浮点型,没有应与标量_mm_extract_epi8/32/64一起使用的插入/提取内在函数。

或存储到数组中并读取该数组。 (print a __m128i variable)。实际上,这确实优化了矢量提取指令。

GNU C向量语法为向量提供了float运算符,例如[] __m256 v = ...;。 MSVC将向量类型定义为具有v[3] = 1.25;成员的并集,以进行每个元素访问。

有类似Agner Fog's (GPL licensed) Vector Class Library的包装器库,它们为它们的向量类型提供了可移植的.m128_f32[]重载,以及运算符operator[] / + / - / {{1 }} 等等。这非常好,特别是对于整数类型,其中对于不同元素宽度具有不同类型的*可以使用正确的大小。 (GNU C本机矢量语法对浮点/双精度矢量进行语法处理,并将<<定义为带符号的int64_t的矢量,但MSVC不提供基于基本v1 + v2类型的运算符。)


您还可以在向量和某种类型的数组之间使用联合类型操作,这在ISO C99和GNU C ++中是安全的,但在ISO C ++中则不是。我认为这在MSVC中也是安全的,因为我认为他们将__m128i定义为普通联合的方式。

但是,不能保证您会从任何这些元素访问方法中获得效率代码。不要使用内部内部循环,如果性能很重要,请查看生成的asm。

答案 1 :(得分:-2)

[edit:有关降低投票权的人,请参阅https://stackoverflow.com/questions/tagged/language-lawyer。此答案对从C ++ 98到当前草案的任何ISO C ++标准均有效。通常认为,诸如“未定义行为”之类的基本概念不需要详细说明,但请参阅http://eel.is/c++draft/defns.undefined和有关SO的各种问题]

由于__m256不是标准类型,也不是用户定义类型的有效名称,因此它已经开始是未定义行为。

实现当然可以添加特定的附加保证,但是Undefined Behavior的含义与ISO C ++有关。