ISO C++没有定义__m256
,因此我们需要查看在支持它们的实现中定义它们行为的内容。
英特尔的内部函数将向量指针(如__m256*
)定义为允许别名任何其他东西,就像ISO C++将char*
定义为允许别名一样。
因此,安全地取消引用__m256*
而不是使用_mm256_load_ps()
对齐加载内部函数是安全的。
但是,特别是对于浮点/双精度,使用内部函数通常更容易,因为它们也会处理从float*
进行转换。对于整数,AVX512加载/存储内部函数被定义为采用void*
,但在此之前,您需要额外的(__m256i*)
,这只是很多杂乱无章的东西。
在gcc中,这是通过使用may_alias
属性来定义__m256
实现的:来自gcc7.3的avxintrin.h
(<immintrin.h>
包含的头文件之一):
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__));
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 如何实现英特尔内置函数只是因为我熟悉这个。我听说 GCC 开发人员选择这种实现是因为它对于与英特尔的兼容性是必需的。
其他行为需要定义Intel的intrinsic函数
使用Intel的API _mm_storeu_si128((__m128i*)&arr[i], vec);
需要创建潜在未对齐的指针,如果您推迟引用它们,则会出错。并且将_mm_storeu_ps
存储到不是4字节对齐的位置需要创建低于对齐的float*
。
仅仅创建未对齐的指针或指向对象外部的指针就是ISO C++中的未定义行为,即使您不引用它们。我猜这允许在异构硬件上实现某些类型的指针检查(可能是代替解除引用时进行的),或者可能无法存储指针的低位。(我不知道是否存在任何特定的硬件,其中由于此UB可以实现更有效的代码。)
但是支持Intel的intrinsic函数的实现必须定义行为,至少对于__m*
类型和float*
/double*
。对于任何正常的现代CPU,包括具有平面内存模型(没有分段)的x86,汇编中的指针只是保留在与数据相同的寄存器中的整数。(m68k具有地址寄存器和数据寄存器,但只要不dereference它们,就从A寄存器中保留不是有效地址的位模式而不会出错。)
另一种方式:访问向量元素。
请注意,像char*
别名规则一样,may_alias
只能单向使用:使用int32_t*
读取__m256
可能不安全。甚至使用float*
读取__m256
也可能不安全。就像char buf[1024];
int *p = (int*)buf;
一样不安全。
请参见GCC AVX _m256i cast to int array leads to wrong values,这是一个真实的例子,展示了GCC如何破坏将int*
指向__m256i vec;
对象的代码。Not a dereferenced __m256i*
;如果仅通过__m256i*
访问__m256i
,那么这是安全的。因为它是一个may_alias
类型,编译器无法推断底层对象是否为__m256i
。这就是整个问题所在,也是为什么可以将其指向int arr[]
或其他内容的原因。
使用char*
进行读/写可以别名任何内容,但是如果你有一个char
对象,则严格别名设置使得通过其他类型读取它是未定义行为。(我不确定x86的主要实现是否定义了这种行为,但你不需要依赖它,因为它们会优化掉将4个字节memcpy
到int32_t
中的操作。你应该使用memcpy
来表示从char[]
缓冲区中的非对齐加载,因为自动向量化使用更宽的类型允许假设int16_t*
具有2字节对齐,从而生成失败的代码:为什么在AMD64上访问不对齐的mmap内存有时会导致段错误?)
char arr[]
可能不是一个很好的类比,因为使用char
对象访问数组实际上涉及到char*
解引用。或许一些结构体中的char
成员会是一个更好的例子。
要插入/提取向量元素,请使用Shuffle intrinsics,SSE2
_mm_insert_epi16
/
_mm_extract_epi16
或SSE4.1 insert /
_mm_extract_epi8/32/64
。对于float,没有应该与标量
float
一起使用的插入/提取intrinsics。
或将其存储到数组中并读取该数组。(
打印一个__m128i变量)。这实际上会优化为矢量提取指令。
GNU C矢量语法为矢量提供了
[]
运算符,例如
__m256 v = ...;
v[3] = 1.25;
。 MSVC将矢量类型定义为带有
.m128_f32 []
成员的联合,用于逐个元素访问。
有一些包装库,例如
Agner Fog的(GPL许可)Vector Class Library,它们为其向量类型提供了可移植的
operator[]
重载,以及
+
/
-
/
*
/
<<
等运算符。这非常好,特别是对于整数类型而言,不同的元素宽度可以使
v1 + v2
使用正确的大小。(GNU C本地向量语法适用于浮点/双精度向量,并将
__m128i
定义为带符号int64_t向量,但MSVC不会在基本的
__m128
类型上提供运算符。)
你也可以在向量和某些类型的数组之间使用联合类型转换,这在ISO C99和GNU C++中是安全的,但在ISO C++中不是安全的。我认为它在MSVC中也是官方安全的,因为我认为他们将 __m128 定义为普通联合体的方式。
尽管如此,无法保证这些元素访问方法会获得高效的代码。请勿在内部循环中使用,并查看汇编结果以提高性能。
float
以外的任何其他类型。 - sandthorn__m256
吗?如果不是,那还有什么意义呢? :) - geza__m256
对象内部且在__m256
生命周期内的浮点数是否违反了严格别名规则? - sandthorn