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
或者只有一种定义的内在方式:
__m256 hwvec2 = _mm256_load_ps(&stack_store[0 * _m256_float_step_sz]);
_mm256_store_ps(&stack_store[1 * _m256_float_step_sz], hwvec2);
ISO C ++没有定义__m256
,因此我们需要查看在支持它们的实现中does定义了它们的行为。
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
。]
不带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*
。
只是creating未对齐的指针,或对象外部的指针,在ISO C ++中是UB,即使您不取消引用它们也是如此。我猜想这允许在执行某些检查的奇异硬件上实现创建指针时(可能不是在取消引用时)创建指针,或者可能无法存储指针的低位指针。 (我不知道是否存在任何特定的硬件,因为此UB,可以使用更高效的代码。)
但是支持Intel内部函数的实现必须定义行为,至少对于__m*
类型和float*
/ double*
。对于以任何普通现代CPU为目标的编译器而言,这都是微不足道的,包括具有平面内存模型(无分段)的x86。 asm中的指针只是与数据保存在同一寄存器中的整数。 (m68k具有地址和数据寄存器,但是只要您不对它们进行解引用,就可以将不是有效地址的位模式保留在A寄存器中,这从不会出错。)
请注意,may_alias
就像char*
别名规则一样,只是一种方式:保证not使用int32_t*
读取__m256
是安全的。使用float*
读取__m256
甚至可能都不安全。就像执行char buf[1024];
int *p = (int*)buf;
并不安全。
通过char*
读/写可以为任何别名,但是当您具有char
object时,严格别名确实使UB可以通过其他类型读取它。 (我不确定x86上的主要实现是否确实定义了该行为,但是您不需要依赖它,因为它们将4个字节的memcpy
优化为int32_t
。您可以并且应该使用[C0 ]表示来自memcpy
缓冲区的未对齐负载,因为允许使用更大类型的自动矢量化假定char[]
的2字节对齐,并且如果不是,则使代码失败:int16_t*
)
要插入/提取矢量元素,请使用随机内在函数,SSE2Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?/ _mm_insert_epi16
或SSE4.1插入/ _mm_extract_epi16
。对于float,没有标量_mm_extract_epi8/32/64
应使用的插入/提取内在函数。
或存储到数组中并读取该数组。 (float
)。实际上,这确实优化了矢量提取指令。
GNU C矢量语法为矢量提供了print a __m128i variable运算符,例如[]
__m256 v = ...;
。 MSVC将向量类型定义为与v[3] = 1.25;
成员的并集,用于每个元素的访问。
[像.m128_f32[]
这样的包装器库为其向量类型提供可移植的Agner Fog's (GPL licensed) Vector Class Library重载,以及运算符operator[]
/+
/-
/ *
等。这非常好,尤其是对于整数类型,其中对于不同的元素宽度具有不同的类型会使<<
以正确的大小工作。 (GNU C本机向量语法针对浮点/双精度向量执行此操作,并将v1 + v2
定义为有符号int64_t的向量,但MSVC不提供基于__m128i
类型的运算符。)
您还可以在向量和某种类型的数组之间使用联合类型对合,这在ISO C99和GNU C ++中是安全的,但在ISO C ++中不安全。我认为这在MSVC中也是安全的,因为我认为他们将__m128
定义为普通联合的方式。
尽管不能保证您会从任何这些元素访问方法中获得效率
代码。不要使用内部内部循环,如果性能很重要,请查看生成的asm。[编辑:有关下降投票者,请参见__m128
。此答案对从C ++ 98到当前草案的任何ISO C ++标准均有效。通常认为,诸如“未定义行为”之类的基本概念不需要详细说明,但请参见https://stackoverflow.com/questions/tagged/language-lawyer和有关SO的各种问题]