硬件 SIMD 向量指针和相应类型之间的“reinterpret_cast”是未定义的行为吗?
Is `reinterpret_cast`ing between hardware SIMD vector pointer and the corresponding type an undefined behavior?
从 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);
hwvec1
和 arr1
是否依赖于 undefined behavior
?
它们是否违反了严格的别名规则? [basic.lval]/11
或者intrinsic只有一种定义方式:
__m256 hwvec2 = _mm256_load_ps(&stack_store[0 * _m256_float_step_sz]);
_mm256_store_ps(&stack_store[1 * _m256_float_step_sz], hwvec2);
[编辑:对于反对者,请参阅 https://whosebug.com/questions/tagged/language-lawyer. This answer is valid for any ISO C++ standard from C++98 to the current draft. It's generally assumed that basic concepts such as Undefined Behavior do not need detailed explanation, but see http://eel.is/c++draft/defns.undefined 和关于 SO 的各种问题]
由于 __m256
不是标准类型,也不是用户定义类型的有效名称,它已经开始成为未定义行为。
实现当然可以添加特定的额外保证,但Undefined Behavior
意味着与 ISO C++ 相关。
ISO C++ 没有定义 __m256
,所以我们需要看看 做了什么 在支持它们的实现上定义它们的行为。
Intel 的内在定义 vector-pointers 就像 __m256*
允许别名一样,就像 ISO C++ 定义 char*
允许别名一样。
所以是的,取消引用 __m256*
而不是使用 _mm256_load_ps()
aligned-load 内在函数是安全的。
但特别是对于 float/double,使用内在函数通常更容易,因为它们也负责从 float*
进行转换。对于整数,AVX512 load/store 内在函数被定义为采用 void*
,但在此之前你需要一个额外的 (__m256i*)
,这只是很多混乱。
在 gcc 中,这是通过使用 may_alias
属性定义 __m256
来实现的:来自 gcc7.3 的 avxintrin.h
([=22= 的 headers 之一] 包括):
/* 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 兼容。
需要定义英特尔内在函数的其他行为
将 Intel 的 API 用于 _mm_storeu_si128( (__m128i*)&arr[i], vec);
需要您创建 potentially-unaligned 指针,如果您对它们进行引用就会出错。 _mm_storeu_ps
到一个不是 4 字节对齐的位置需要创建一个 under-aligned float*
.
只是 创建 未对齐的指针,或 object 之外的指针,在 ISO C++ 中是 UB,即使您不取消引用它们。 我想这允许在奇异的硬件上实现,这些硬件在创建指针时对指针进行某种检查(可能而不是在取消引用时),或者可能无法存储指针的低位。 (我不知道是否存在任何特定的硬件,因为这个 UB 可以实现更高效的代码。)
但是支持英特尔内在函数的实现必须定义行为,至少对于 __m*
类型和 float*
/double*
。这对于针对任何普通现代 CPU 的编译器来说是微不足道的,包括具有平面内存模型(无分段)的 x86; asm 中的指针只是保存在与数据相同的寄存器中的整数。 (m68k 有地址和数据寄存器,但它永远不会因为在 A 寄存器中保留 bit-patterns 而不是有效地址而出错,只要你不取消引用它们。)
换一种方式:向量的元素访问。
请注意 may_alias
,就像 char*
别名规则一样,只有一种方式:它是 而不是 保证可以安全使用int32_t*
读一个__m256
。使用 float*
读取 __m256
甚至可能不安全。就像做 char buf[1024];
int *p = (int*)buf;
.
不安全一样
参见GCC AVX _m256i cast to int array leads to wrong values for a real-world example of GCC breaking code that points an int*
into a __m256i vec;
object. Not a dereferenced __m256i*
;如果只有 __m256i
访问是通过 __m256i*
,那将是安全的。因为它是 may_alias
类型,所以编译器无法推断出底层 object 是 __m256i
;这就是重点,以及为什么将它指向 int arr[]
或其他什么是安全的。
Reading/writing 通过 char*
可以别名任何东西,但是当你有 char
object, strict-aliasing 确实使它成为通过其他类型读取它的 UB。 (我不确定 x86 上的主要实现是否确实定义了该行为,但您不需要依赖它,因为它们将 4 个字节的 memcpy
优化为 int32_t
。您可以应该使用 memcpy
来表示来自 char[]
缓冲区的未对齐加载,因为允许具有更宽类型的 auto-vectorization 假定 int16_t*
的 2 字节对齐,并编写代码如果不是则失败:Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?)
A char arr[]
可能不是一个很好的类比,因为 arr[i]
是根据 *(arr+i)
定义的,所以实际上有一个 char*
deref 涉及访问数组作为 char
objects。也许结构的某些 char
成员会是一个更好的例子。
到insert/extract 向量元素,使用shuffle intrinsics、SSE2 _mm_insert_epi16
/ _mm_extract_epi16
或SSE4.1 insert / _mm_extract_epi8/32/64
。对于浮点数,没有应该与标量 float
.
一起使用的 insert/extract 内在函数
或者存储到数组中,读取数组。 (print a __m128i variable)。这实际上优化了向量提取指令。
GNU C 向量语法为向量提供 []
运算符,如 __m256 v = ...;
v[3] = 1.25;
。 MSVC 将矢量类型定义为具有 .m128_f32[]
成员的联合,用于 per-element 访问。
有像 Agner Fog's (GPL licensed) Vector Class Library 这样的包装库,它们为它们的向量类型提供可移植的 operator[]
重载,还有运算符 +
/ -
/ *
/ <<
等等。这非常好,特别是对于整数类型,对于不同的元素宽度具有不同的类型使得 v1 + v2
使用正确的 s泽。 (GNU C 本机向量语法为 float/double 向量执行此操作,并将 __m128i
定义为带符号 int64_t 的向量,但 MSVC 不提供基于 __m128
类型的运算符.)
您还可以在向量和某种类型的数组之间使用并集 type-punning,这在 ISO C99 和 GNU C++ 中是安全的,但在 ISO C++ 中不是。我认为它在 MSVC 中也是正式安全的,因为我认为他们将 __m128
定义为普通联合的方式。
不过,不能保证您会从这些 element-access 方法中的任何一个中获得 高效的 代码。不要使用 inside 内部循环,如果性能很重要,请查看生成的 asm。
从 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);
hwvec1
和 arr1
是否依赖于 undefined behavior
?
它们是否违反了严格的别名规则? [basic.lval]/11
或者intrinsic只有一种定义方式:
__m256 hwvec2 = _mm256_load_ps(&stack_store[0 * _m256_float_step_sz]);
_mm256_store_ps(&stack_store[1 * _m256_float_step_sz], hwvec2);
[编辑:对于反对者,请参阅 https://whosebug.com/questions/tagged/language-lawyer. This answer is valid for any ISO C++ standard from C++98 to the current draft. It's generally assumed that basic concepts such as Undefined Behavior do not need detailed explanation, but see http://eel.is/c++draft/defns.undefined 和关于 SO 的各种问题]
由于 __m256
不是标准类型,也不是用户定义类型的有效名称,它已经开始成为未定义行为。
实现当然可以添加特定的额外保证,但Undefined Behavior
意味着与 ISO C++ 相关。
ISO C++ 没有定义 __m256
,所以我们需要看看 做了什么 在支持它们的实现上定义它们的行为。
Intel 的内在定义 vector-pointers 就像 __m256*
允许别名一样,就像 ISO C++ 定义 char*
允许别名一样。
所以是的,取消引用 __m256*
而不是使用 _mm256_load_ps()
aligned-load 内在函数是安全的。
但特别是对于 float/double,使用内在函数通常更容易,因为它们也负责从 float*
进行转换。对于整数,AVX512 load/store 内在函数被定义为采用 void*
,但在此之前你需要一个额外的 (__m256i*)
,这只是很多混乱。
在 gcc 中,这是通过使用 may_alias
属性定义 __m256
来实现的:来自 gcc7.3 的 avxintrin.h
([=22= 的 headers 之一] 包括):
/* 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 兼容。
需要定义英特尔内在函数的其他行为
将 Intel 的 API 用于 _mm_storeu_si128( (__m128i*)&arr[i], vec);
需要您创建 potentially-unaligned 指针,如果您对它们进行引用就会出错。 _mm_storeu_ps
到一个不是 4 字节对齐的位置需要创建一个 under-aligned float*
.
只是 创建 未对齐的指针,或 object 之外的指针,在 ISO C++ 中是 UB,即使您不取消引用它们。 我想这允许在奇异的硬件上实现,这些硬件在创建指针时对指针进行某种检查(可能而不是在取消引用时),或者可能无法存储指针的低位。 (我不知道是否存在任何特定的硬件,因为这个 UB 可以实现更高效的代码。)
但是支持英特尔内在函数的实现必须定义行为,至少对于 __m*
类型和 float*
/double*
。这对于针对任何普通现代 CPU 的编译器来说是微不足道的,包括具有平面内存模型(无分段)的 x86; asm 中的指针只是保存在与数据相同的寄存器中的整数。 (m68k 有地址和数据寄存器,但它永远不会因为在 A 寄存器中保留 bit-patterns 而不是有效地址而出错,只要你不取消引用它们。)
换一种方式:向量的元素访问。
请注意 may_alias
,就像 char*
别名规则一样,只有一种方式:它是 而不是 保证可以安全使用int32_t*
读一个__m256
。使用 float*
读取 __m256
甚至可能不安全。就像做 char buf[1024];
int *p = (int*)buf;
.
参见GCC AVX _m256i cast to int array leads to wrong values for a real-world example of GCC breaking code that points an int*
into a __m256i vec;
object. Not a dereferenced __m256i*
;如果只有 __m256i
访问是通过 __m256i*
,那将是安全的。因为它是 may_alias
类型,所以编译器无法推断出底层 object 是 __m256i
;这就是重点,以及为什么将它指向 int arr[]
或其他什么是安全的。
Reading/writing 通过 char*
可以别名任何东西,但是当你有 char
object, strict-aliasing 确实使它成为通过其他类型读取它的 UB。 (我不确定 x86 上的主要实现是否确实定义了该行为,但您不需要依赖它,因为它们将 4 个字节的 memcpy
优化为 int32_t
。您可以应该使用 memcpy
来表示来自 char[]
缓冲区的未对齐加载,因为允许具有更宽类型的 auto-vectorization 假定 int16_t*
的 2 字节对齐,并编写代码如果不是则失败:Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?)
A char arr[]
可能不是一个很好的类比,因为 arr[i]
是根据 *(arr+i)
定义的,所以实际上有一个 char*
deref 涉及访问数组作为 char
objects。也许结构的某些 char
成员会是一个更好的例子。
到insert/extract 向量元素,使用shuffle intrinsics、SSE2 _mm_insert_epi16
/ _mm_extract_epi16
或SSE4.1 insert / _mm_extract_epi8/32/64
。对于浮点数,没有应该与标量 float
.
或者存储到数组中,读取数组。 (print a __m128i variable)。这实际上优化了向量提取指令。
GNU C 向量语法为向量提供 []
运算符,如 __m256 v = ...;
v[3] = 1.25;
。 MSVC 将矢量类型定义为具有 .m128_f32[]
成员的联合,用于 per-element 访问。
有像 Agner Fog's (GPL licensed) Vector Class Library 这样的包装库,它们为它们的向量类型提供可移植的 operator[]
重载,还有运算符 +
/ -
/ *
/ <<
等等。这非常好,特别是对于整数类型,对于不同的元素宽度具有不同的类型使得 v1 + v2
使用正确的 s泽。 (GNU C 本机向量语法为 float/double 向量执行此操作,并将 __m128i
定义为带符号 int64_t 的向量,但 MSVC 不提供基于 __m128
类型的运算符.)
您还可以在向量和某种类型的数组之间使用并集 type-punning,这在 ISO C99 和 GNU C++ 中是安全的,但在 ISO C++ 中不是。我认为它在 MSVC 中也是正式安全的,因为我认为他们将 __m128
定义为普通联合的方式。
不过,不能保证您会从这些 element-access 方法中的任何一个中获得 高效的 代码。不要使用 inside 内部循环,如果性能很重要,请查看生成的 asm。