做这样的事情是合法的吗?
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
s?
他们违反了严格的别名规则吗? [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
,因此我们需要查看在支持它们的实现上定义它们的行为的内容。
英特尔的内在函数将像__m256*
这样的矢量指针定义为允许别名的别名,就像ISO C ++将char*
定义为允许别名一样。
所以是的,取消引用__m256*
而不是使用_mm256_load_ps()
对齐负载内在是安全的。
但特别是对于浮动/双重,它通常更容易使用内在因素,因为它们也照顾来自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如何实现英特尔的内在函数,因为这是我所熟悉的。我从gcc开发人员那里听说他们选择了这个实现,因为它是与英特尔兼容的必要条件。
对_mm_storeu_si128( (__m128i*)&arr[i], vec);
使用英特尔的API要求您创建可能未对齐的指针,如果您对它们进行了修改则会出错。并且_mm_storeu_ps
到一个非4字节对齐的位置需要创建一个未对齐的float*
。
只是在ISO C ++中创建未对齐的指针或指针外的指针即使您不取消引用它们也是UB。我想这允许在异常硬件上实现,这些硬件在创建它们时可能会对指针进行某种检查(可能不是在解除引用时),或者可能无法存储指针的低位。 (我不知道是否存在任何特定的硬件,因为这个UB可能有更高效的代码。)
但支持英特尔内在函数的实现必须定义行为,至少对于__m*
类型和float*
/ double*
。对于任何针对任何普通现代CPU的编译器来说,这都是微不足道的,包括具有平坦内存模型的x86(无分段); asm中的指针只是与数据保持在同一寄存器中的整数。 (m68k具有地址与数据寄存器,但只要不解析它们,它就不会因保留A寄存器中无效地址的位模式而出错。)
请注意,may_alias
,就像char*
别名规则一样,只有一种方式:使用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?)
要插入/提取矢量元素,请使用shuffle内在函数,SSE2 _mm_insert_epi16
/ _mm_extract_epi16
或SSE4.1 insert / _mm_extract_epi8/32/64
。对于float,没有插入/提取内在函数,你应该使用标量float
。
或者存储到数组并读取数组。 (print a __m128i variable)。这确实优化了向量提取指令。
GNU C矢量语法为矢量提供[]
运算符,如__m256 v = ...;
v[3] = 1.25;
。 MSVC将矢量类型定义为与每个元素访问的.m128_f32[]
成员的并集。
有像Agner Fog's (GPL licensed) Vector Class Library这样的包装库,它们为它们的矢量类型提供了便携式operator[]
重载,以及运算符+
/ -
/ *
/ <<
等等。它非常好,特别是对于不同元素宽度具有不同类型的整数类型,v1 + v2
的工作尺寸合适。 (GNU C本机向量语法对float / double向量执行此操作,并将__m128i
定义为signed int64_t的向量,但MSVC不提供基本__m128
类型的运算符。)
您还可以在向量和某种类型的数组之间使用并集类型,这在ISO C99和GNU C ++中是安全的,但在ISO C ++中则不然。我认为它在MSVC中也是正式安全的,因为我认为他们将__m128
定义为正常联盟的方式。
但是,无法保证您从任何这些元素访问方法中获得有效的代码。如果性能很重要,请不要使用内部循环,并查看生成的asm。
[编辑:对于downvoter,请参阅https://stackoverflow.com/questions/tagged/language-lawyer。这个答案适用于从C ++ 98到当前草案的任何ISO C ++标准。通常假设未定义行为等基本概念不需要详细解释,但请参阅http://eel.is/c++draft/defns.undefined和SO上的各种问题]
由于__m256
不是标准类型,它已经开始是未定义的行为,也不是用户定义类型的有效名称。
实现当然可以添加特定的附加保证,但Undefined Behavior
意味着与ISO C ++相关。