在硬件向量指针和相应类型之间`reinterpret_cast`是一个未定义的行为吗?

问题描述 投票:1回答:2

做这样的事情是合法的吗?

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 behaviors?

他们违反了严格的别名规则吗? [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

c++ x86 language-lawyer intrinsics type-punning
2个回答
3
投票

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。


-2
投票

[编辑:对于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 ++相关。

© www.soinside.com 2019 - 2024. All rights reserved.