如何 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};
}

初始化 __m128iconstexpr 之类的一些巧妙(且可移植)的方法吗?

实际使用

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++抽象机中没有寄存器。)