c++ - 硬件 SIMD vector 指针和相应类型之间的 `reinterpret_cast` 是未定义的行为吗?

标签 c++ x86 language-lawyer undefined-behavior intrinsics

reinterpret_cast合法吗? float*__m256*和访问 float对象通过不同的指针类型?

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

最佳答案

ISO C++ 没有定义 __m256 ,所以我们需要看看是什么在支持它们的实现上定义了它们的行为。

英特尔的内在函数将 vector 指针(如 __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 native vector 为其标量类型设置别名,例如即使没有 may_alias ,您也可以安全地在 float* 和假设的 v8sf 类型之间进行转换。但是 may_alias 可以安全地从 int[]char[] 或其他数组中加载。

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

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

将英特尔的 API 用于 _mm_storeu_si128( (__m128i*)&arr[i], vec); 要求您创建可能未对齐的指针,如果您尊重它们,这些指针就会出错。并且 _mm_storeu_ps 到非 4 字节对齐的位置需要创建一个未对齐的 float*

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

但是支持英特尔内在函数的实现必须定义行为,至少对于 __m* 类型和 float*/double* 。这对于针对任何普通现代 CPU 的编译器来说是微不足道的,包括具有扁平内存模型(无分段)的 x86; asm 中的指针只是与数据保存在相同寄存器中的整数。 (m68k 具有地址和数据寄存器,但它永远不会因为在 A 寄存器中保留不是有效地址的位模式而出错,只要您不取消引用它们。)

反过来说: vector 的元素访问。

请注意, may_aliaschar* 别名规则一样,只有一种方式 :不能保证使用 int32_t* 读取 __m256 是安全的。使用 float* 读取 __m256 甚至可能不安全。就像执行 char buf[1024]; int *p = (int*)buf; 不安全一样。

通过 char* 读/写可以为任何东西设置别名,但是当你有一个 char 对象时,严格别名确实使它成为通过其他类型读取它的 UB。 (我不确定 x86 上的主要实现是否确实定义了该行为,但您不需要依赖它,因为它们将 4 个字节的 memcpy 优化为 int32_t 。您可以并且应该使用 memcpy 来表达未对齐的负载来自 char[] 缓冲区,因为允许使用更宽类型的自动矢量化为 int16_t* 假设 2 字节对齐,如果不是,则使代码失败: Why does unaligned access to mmap'ed memory sometimes segfault on AMD64? )

要插入/提取 vector 元素,请使用 shuffle 内在函数、SSE2 _mm_insert_epi16/_mm_extract_epi16 或 SSE4.1 insert/_mm_extract_epi8/32/64 。对于 float,没有插入/提取内部函数可以与标量 float 一起使用。

或者存储到一个数组并读取该数组。 ( print a __m128i variable )。这实际上优化了 vector 提取指令。

GNU C vector 语法为 vector 提供了 [] 运算符,例如 __m256 v = ...; v[3] = 1.25; 。 MSVC 将 vector 类型定义为带有 .m128_f32[] 成员的 union ,用于按元素访问。

有像 Agner Fog's (GPL licensed) Vector Class Library 这样的包装库,它们为其 vector 类型提供可移植的 operator[] 重载,以及运算符 +/-/*/<< 等等。这非常好,特别是对于整数类型,其中不同元素宽度的不同类型使 v1 + v2 以正确的大小工作。 (GNU C native vector 语法对浮点/双 vector 执行此操作,并将 __m128i 定义为带符号 int64_t 的 vector ,但 MSVC 不提供基本 __m128 类型的运算符。)

您还可以在 vector 和某种类型的数组之间使用 union 类型双关,这在 ISO C99 和 GNU C++ 中是安全的,但在 ISO C++ 中不安全。我认为它在 MSVC 中也是正式安全的,因为我认为他们将 __m128 定义为正常 union 的方式。

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

关于c++ - 硬件 SIMD vector 指针和相应类型之间的 `reinterpret_cast` 是未定义的行为吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52112605/

相关文章:

c++ - 调用 delete [] 会使程序崩溃,但在调试时不会

c++ - 重构仅在最内层范围内不同的多个重复函数

c - 在x86上给出无分支FP最小值和最大值的指令是什么?

assembly lea : invalid effective address

c++ - 不可能的隐式 move 操作?

c++ - 调用成员函数时出错

c++ - 程序在在线 IDE 上表现异常

assembly - 在汇编中生成随机数

c++ - C++ 重载解析规则中的缺陷?

c++ - 模板别名和相关名称