如何 constexpr 初始化内部 SSE/AVX 寄存器?
How to constexpr initialize intrinsic SSE/AVX register?
考虑像 __m128i xmm_stuff = _mm_set_epi32(1, 2, 3, 4);
这样的东西,它可能是 const
但不是 consexpr
,因为编译器实现中有一个基础 reinterpret_cast
。事实上,内在函数是未声明的函数 constexpr
.
例如,来自 clang-12 的 immintrin.h:
static __inline__ __m128i __DEFAULT_FN_ATTRS
_mm_set_epi32(int __i3, int __i2, int __i1, int __i0)
{
return __extension__ (__m128i)(__v4si){ __i0, __i1, __i2, __i3};
}
初始化 __m128i
和 constexpr
之类的一些巧妙(且可移植)的方法吗?
实际使用
constexpr std::array<int32_t,4> input = {1, 2, 3, 4};
const __m128i xmm_input = _mm_load_si128(reinterpret_cast<const __m128i*>(input.data()));
想要的用法,更简洁明了:
constexpr __m128i xmm_input = {1, 2, 3, 4};
compile-time 中不存在寄存器。无论这些 AVX 指令在做什么,compile-time 结果都必须在运行时加载到寄存器中。所以你应该只使用普通的 C++ 代码计算 compile-time 值(也许使用 if (std::is_constant_evaluated())
到 fence off such blocks of code 允许你将两者放在同一个函数中)然后加载 constexpr
将值转换为 AVX 对象。
Not that it would change anything semantically or performance-wise
正确,只需像大多数代码一样使用 const __m128i
。
我没有看到 constexpr
对 use-case 有任何好处,只是付出没有收获。
也许如果有办法,它可以让您在静态存储(全局或 static
)中初始化向量,而不会像使用 _mm_set
时那样出现通常的混乱情况,其中编译器保留 space in .bss
并在 run-time 处运行构造函数以从 .rodata.
中的匿名常量复制
(是的,gcc/clang/MSVC 真的很糟糕;godbolt。不要使用 static const __m128i
或在全局范围内。使用 const __m128i foo = _mm_set_epi32()
或任何内部函数;编译器 + 链接器将消除重复项,例如字符串文字。或者在函数内部使用带有 alignas(16)
和 _mm_load_si128
的普通数组,如果效果更好的话。)
just curious why in the year 2022 I can't declare constexpr __m128i
您可以声明 constexpr __m128i
,只是不能可移植初始化它1,因为 像 _mm_set_*
这样的英特尔内在函数是在 2000 年之前定义的(对于 MMX,然后是 SSE1),而不是 constexpr
。 (后来的内部函数仍然遵循为 SSE1 建立的相同模式。)请记住,在 C/C++ 术语中,它们是恰好内联的实际函数。 (或者围绕 __builtin
的宏函数可以为成为立即数的操作数获取 compile-time 常量。)
附注1:在C++20中,GCC允许你使用constexpr auto y = std::bit_cast<__m128i>(x);
,如https://godbolt.org/z/YGMGM69qs所示。其他编译器接受 bit_cast<float>
或其他,但不接受 __m128
,因此这可能是 GCC 的实现细节。在任何情况下,它都不会节省打字时间,而且即使它可以移植到 clang 和 MSVC 也没有多大用处。
没有什么意义,因为 像 _mm_add_epi32
这样的内在函数也不是 constexpr
,而且你不能 便携做v1 += v2
;在 GNU C/C++ 中编译(到 paddq
)。
带有 non-portable 大括号初始值设定项的示例; 不要这样做:
#include <immintrin.h>
__m128i foo() {
// different meaning in GCC/clang vs. MSVC
constexpr __m128i v = {1, 2};
return v;
}
GCC11.2 -O3 asm 输出 (Godbolt) - 两个 long long
一半,按照 GCC/clang 的方式将 __m128i
定义为 typdef long long __m128i __attribute__((vector_size(16),may_alias))
foo():
movdqa xmm0, XMMWORD PTR .LC0[rip]
ret
.LC0:
.quad 1
.quad 2
MSVC 19.30 - 16x int8_t 的前两个字节 - MSVC 将 __m128i
定义为各种 element-widths 数组的并集,显然首先是 char[16]
。
__xmm@00000000000000000000000000000201 DB 01H, 02H, 00H, 00H, 00H, 00H, 00H
DB 00H, 00H, 00H, 00H, 00H, 00H, 00H, 00H, 00H
__m128i foo(void) PROC ; foo, COMDAT
movdqa xmm0, XMMWORD PTR __xmm@00000000000000000000000000000201
ret 0
__m128i foo(void) ENDP ; foo
因此您可以将向量初始化为 {0}
,并在 gcc/clang 上获得与在 MSVC 上相同的结果,或者我猜任何 {0..255}
。但这仍在利用每个特定编译器的 实现细节 ,而不是严格使用英特尔记录的内在函数 API.
并且 MS says 你永远不应该直接访问联合的那些字段(MSVC 定义 __m128i
的方式)。
GCC 确实为 GNU C 本地向量定义了语义; GCC / clang 在其可移植 vector extensions 之上实现了 Intel 内在函数 API(包括 __m128i
),其工作方式类似于结构或 class 以及 + - & | 等运算符* / [] 等等。
另见 回复:什么是 __m128i
对象及其工作原理。
术语:__m128i
不是寄存器。
它是一个类似于 int
的 C++ 对象,可以放入寄存器中,如果启用优化,编译器通常会将变量的值跨语句保存在寄存器中。
但是您仍然可以将它的地址、memcpy 放入/取出(部分),否则会弄乱它的对象表示,所有这些都根据 C++ 抽象机的规则(包括向量扩展)工作). (不过,与使用 shuffle 内在函数相比,生成的 asm 可能不是很有效!)
你可以创建一个数组甚至 std::vector<__m128i>
(使用 C++17 进行对齐分配),显然那些 __m128i
对象不能都在寄存器中。
更好的术语:“初始化 AVX 内在向量”。这些类型表示数据的 SIMD 向量, 可以 加载到向量寄存器中。就像 int
代表一个 fixed-width 整数,可以将其加载到整数寄存器中。使用 __m128i
编写代码的方式很常见,所有这些对象都是本地对象,实际上可以存在于寄存器中,希望甚至不会得到 spilled/reloaded,但这是由于它的使用方式,而不是它是什么。
当您谈论初始化一个 int
对象时,您谈论的是对象,而不是寄存器。 (特别是 for constexpr
;C++抽象机中没有寄存器。)
考虑像 __m128i xmm_stuff = _mm_set_epi32(1, 2, 3, 4);
这样的东西,它可能是 const
但不是 consexpr
,因为编译器实现中有一个基础 reinterpret_cast
。事实上,内在函数是未声明的函数 constexpr
.
例如,来自 clang-12 的 immintrin.h:
static __inline__ __m128i __DEFAULT_FN_ATTRS
_mm_set_epi32(int __i3, int __i2, int __i1, int __i0)
{
return __extension__ (__m128i)(__v4si){ __i0, __i1, __i2, __i3};
}
初始化 __m128i
和 constexpr
之类的一些巧妙(且可移植)的方法吗?
实际使用
constexpr std::array<int32_t,4> input = {1, 2, 3, 4};
const __m128i xmm_input = _mm_load_si128(reinterpret_cast<const __m128i*>(input.data()));
想要的用法,更简洁明了:
constexpr __m128i xmm_input = {1, 2, 3, 4};
compile-time 中不存在寄存器。无论这些 AVX 指令在做什么,compile-time 结果都必须在运行时加载到寄存器中。所以你应该只使用普通的 C++ 代码计算 compile-time 值(也许使用 if (std::is_constant_evaluated())
到 fence off such blocks of code 允许你将两者放在同一个函数中)然后加载 constexpr
将值转换为 AVX 对象。
Not that it would change anything semantically or performance-wise
正确,只需像大多数代码一样使用 const __m128i
。
我没有看到 constexpr
对 use-case 有任何好处,只是付出没有收获。
也许如果有办法,它可以让您在静态存储(全局或 static
)中初始化向量,而不会像使用 _mm_set
时那样出现通常的混乱情况,其中编译器保留 space in .bss
并在 run-time 处运行构造函数以从 .rodata.
(是的,gcc/clang/MSVC 真的很糟糕;godbolt。不要使用 static const __m128i
或在全局范围内。使用 const __m128i foo = _mm_set_epi32()
或任何内部函数;编译器 + 链接器将消除重复项,例如字符串文字。或者在函数内部使用带有 alignas(16)
和 _mm_load_si128
的普通数组,如果效果更好的话。)
just curious why in the year 2022 I can't declare
constexpr __m128i
您可以声明 constexpr __m128i
,只是不能可移植初始化它1,因为 像 _mm_set_*
这样的英特尔内在函数是在 2000 年之前定义的(对于 MMX,然后是 SSE1),而不是 constexpr
。 (后来的内部函数仍然遵循为 SSE1 建立的相同模式。)请记住,在 C/C++ 术语中,它们是恰好内联的实际函数。 (或者围绕 __builtin
的宏函数可以为成为立即数的操作数获取 compile-time 常量。)
附注1:在C++20中,GCC允许你使用constexpr auto y = std::bit_cast<__m128i>(x);
,如https://godbolt.org/z/YGMGM69qs所示。其他编译器接受 bit_cast<float>
或其他,但不接受 __m128
,因此这可能是 GCC 的实现细节。在任何情况下,它都不会节省打字时间,而且即使它可以移植到 clang 和 MSVC 也没有多大用处。
没有什么意义,因为 像 _mm_add_epi32
这样的内在函数也不是 constexpr
,而且你不能 便携做v1 += v2
;在 GNU C/C++ 中编译(到 paddq
)。
带有 non-portable 大括号初始值设定项的示例; 不要这样做:
#include <immintrin.h>
__m128i foo() {
// different meaning in GCC/clang vs. MSVC
constexpr __m128i v = {1, 2};
return v;
}
GCC11.2 -O3 asm 输出 (Godbolt) - 两个 long long
一半,按照 GCC/clang 的方式将 __m128i
定义为 typdef long long __m128i __attribute__((vector_size(16),may_alias))
foo():
movdqa xmm0, XMMWORD PTR .LC0[rip]
ret
.LC0:
.quad 1
.quad 2
MSVC 19.30 - 16x int8_t 的前两个字节 - MSVC 将 __m128i
定义为各种 element-widths 数组的并集,显然首先是 char[16]
。
__xmm@00000000000000000000000000000201 DB 01H, 02H, 00H, 00H, 00H, 00H, 00H
DB 00H, 00H, 00H, 00H, 00H, 00H, 00H, 00H, 00H
__m128i foo(void) PROC ; foo, COMDAT
movdqa xmm0, XMMWORD PTR __xmm@00000000000000000000000000000201
ret 0
__m128i foo(void) ENDP ; foo
因此您可以将向量初始化为 {0}
,并在 gcc/clang 上获得与在 MSVC 上相同的结果,或者我猜任何 {0..255}
。但这仍在利用每个特定编译器的 实现细节 ,而不是严格使用英特尔记录的内在函数 API.
并且 MS says 你永远不应该直接访问联合的那些字段(MSVC 定义 __m128i
的方式)。
GCC 确实为 GNU C 本地向量定义了语义; GCC / clang 在其可移植 vector extensions 之上实现了 Intel 内在函数 API(包括 __m128i
),其工作方式类似于结构或 class 以及 + - & | 等运算符* / [] 等等。
另见 __m128i
对象及其工作原理。
术语:__m128i
不是寄存器。
它是一个类似于 int
的 C++ 对象,可以放入寄存器中,如果启用优化,编译器通常会将变量的值跨语句保存在寄存器中。
但是您仍然可以将它的地址、memcpy 放入/取出(部分),否则会弄乱它的对象表示,所有这些都根据 C++ 抽象机的规则(包括向量扩展)工作). (不过,与使用 shuffle 内在函数相比,生成的 asm 可能不是很有效!)
你可以创建一个数组甚至 std::vector<__m128i>
(使用 C++17 进行对齐分配),显然那些 __m128i
对象不能都在寄存器中。
更好的术语:“初始化 AVX 内在向量”。这些类型表示数据的 SIMD 向量, 可以 加载到向量寄存器中。就像 int
代表一个 fixed-width 整数,可以将其加载到整数寄存器中。使用 __m128i
编写代码的方式很常见,所有这些对象都是本地对象,实际上可以存在于寄存器中,希望甚至不会得到 spilled/reloaded,但这是由于它的使用方式,而不是它是什么。
当您谈论初始化一个 int
对象时,您谈论的是对象,而不是寄存器。 (特别是 for constexpr
;C++抽象机中没有寄存器。)