是否在 C++ 中转换为 simd 类型的未定义行为?
Is casting to simd-type undefined behaviour in C++?
在 simd 教程中,我找到了以下代码片段。
void simd(float* a, int N)
{
// We assume N % 4 == 0.
int nb_iters = N / 4;
__m128* ptr = reinterpret_cast<__m128*>(a); // (*)
for (int i = 0; i < nb_iters; ++i, ++ptr, a += 4)
_mm_store_ps(a, _mm_sqrt_ps(*ptr));
}
现在我的问题是,带有 (*) 未定义行为的行是吗?由于 (https://en.cppreference.com/w/cpp/language/reinterpret_cast)
的以下规范
Whenever an attempt is made to read or modify the stored value of an object of type DynamicType through a glvalue of type AliasedType, the behavior is undefined unless one of the following is true:
- AliasedType and DynamicType are similar.
- AliasedType is the (possibly cv-qualified) signed or unsigned variant of DynamicType.
- AliasedType is std::byte, (since C++17)char, or unsigned char: this permits examination of the object representation of any object as an array of bytes.
在这种情况下,如何防止未定义的行为?我知道我可以 std::memcopy,但性能损失会使 simd 变得无用,或者我错了吗?
编辑:请查看副本中的答案(and/or 彼得的答案在这里)。我在下面写的内容在技术上是正确的,但在实践中并不真正相关。
是的,根据 C++ 标准,那将是未定义的行为。您的编译器可能仍将其作为扩展正确处理(因为 SIMD 类型和内在函数首先不是 C++ 标准的一部分)。
要在不影响速度的情况下安全正确地执行此操作,您可以使用内部函数将 4 个浮点数直接从内存加载到 128 位寄存器中:
__m128 reg = _mm_load_ps(a);
请参阅 Intel Intrinsics Guide 了解重要的对齐约束:
__m128 _mm_load_ps (float const* mem_addr)
Load 128-bits (composed of 4 packed single-precision (32-bit) floating-point elements) from memory into dst
. mem_addr
must be aligned on a 16-byte boundary or a general-protection exception may be generated.
英特尔的内在函数 API 确实定义了转换为 __m128*
和取消引用的行为:它与同一指针上的 _mm_load_ps
相同。
对于 float*
和 double*
,load/store 内在函数基本上用于包装此重新解释转换并将对齐信息传达给编译器。
如果支持_mm_load_ps()
,实现还必须定义问题中代码的行为。
我不知道这是否真的有记载;可能在 Intel 教程或白皮书中,但这是所有编译器的一致行为,我认为大多数人会同意 没有 定义此行为的编译器不完全支持英特尔的内在函数 API.
__m128
类型被定义为 may_alias
1,所以像 char*
你可以将 __m128*
指向任何东西,包括 int[]
或任意结构,并且 通过它加载或存储而不违反严格别名 。 (只要它按 16 对齐,否则你确实需要 _mm_loadu_ps
,或者用类似 GNU C 的 aligned(1)
属性声明的自定义向量类型)。
脚注 1:__attribute__((vector_size(16), may_alias))
在 GNU C 中,MSVC 不进行基于类型的别名分析。
在 simd 教程中,我找到了以下代码片段。
void simd(float* a, int N)
{
// We assume N % 4 == 0.
int nb_iters = N / 4;
__m128* ptr = reinterpret_cast<__m128*>(a); // (*)
for (int i = 0; i < nb_iters; ++i, ++ptr, a += 4)
_mm_store_ps(a, _mm_sqrt_ps(*ptr));
}
现在我的问题是,带有 (*) 未定义行为的行是吗?由于 (https://en.cppreference.com/w/cpp/language/reinterpret_cast)
的以下规范Whenever an attempt is made to read or modify the stored value of an object of type DynamicType through a glvalue of type AliasedType, the behavior is undefined unless one of the following is true:
- AliasedType and DynamicType are similar.
- AliasedType is the (possibly cv-qualified) signed or unsigned variant of DynamicType.
- AliasedType is std::byte, (since C++17)char, or unsigned char: this permits examination of the object representation of any object as an array of bytes.
在这种情况下,如何防止未定义的行为?我知道我可以 std::memcopy,但性能损失会使 simd 变得无用,或者我错了吗?
编辑:请查看副本中的答案(and/or 彼得的答案在这里)。我在下面写的内容在技术上是正确的,但在实践中并不真正相关。
是的,根据 C++ 标准,那将是未定义的行为。您的编译器可能仍将其作为扩展正确处理(因为 SIMD 类型和内在函数首先不是 C++ 标准的一部分)。
要在不影响速度的情况下安全正确地执行此操作,您可以使用内部函数将 4 个浮点数直接从内存加载到 128 位寄存器中:
__m128 reg = _mm_load_ps(a);
请参阅 Intel Intrinsics Guide 了解重要的对齐约束:
__m128 _mm_load_ps (float const* mem_addr)
Load 128-bits (composed of 4 packed single-precision (32-bit) floating-point elements) from memory into
dst
.mem_addr
must be aligned on a 16-byte boundary or a general-protection exception may be generated.
英特尔的内在函数 API 确实定义了转换为 __m128*
和取消引用的行为:它与同一指针上的 _mm_load_ps
相同。
对于 float*
和 double*
,load/store 内在函数基本上用于包装此重新解释转换并将对齐信息传达给编译器。
如果支持_mm_load_ps()
,实现还必须定义问题中代码的行为。
我不知道这是否真的有记载;可能在 Intel 教程或白皮书中,但这是所有编译器的一致行为,我认为大多数人会同意 没有 定义此行为的编译器不完全支持英特尔的内在函数 API.
__m128
类型被定义为 may_alias
1,所以像 char*
你可以将 __m128*
指向任何东西,包括 int[]
或任意结构,并且 通过它加载或存储而不违反严格别名 。 (只要它按 16 对齐,否则你确实需要 _mm_loadu_ps
,或者用类似 GNU C 的 aligned(1)
属性声明的自定义向量类型)。
脚注 1:__attribute__((vector_size(16), may_alias))
在 GNU C 中,MSVC 不进行基于类型的别名分析。