如何在不使用任何 SSE 指令的情况下设置 __m128i?

How can I set __m128i without using of any SSE instruction?

我有很多函数使用相同的常量 __m128i 值。 例如:

const __m128i K8 = _mm_setr_epi8(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
const __m128i K16 = _mm_setr_epi16(1, 2, 3, 4, 5, 6, 7, 8);
const __m128i K32 = _mm_setr_epi32(1, 2, 3, 4);

所以我想将所有这些常量存储在一个地方。 但是有一个问题:我在 运行 时间内检查存在的 CPU 扩展名。 如果 CPU 不支持例如 SSE(或 AVX),那么在常量初始化期间程序将崩溃。

那么是否可以在不使用 SSE 的情况下初始化这些常量?

我建议将全局初始化数据定义为标量数据,然后将其本地加载到 const __m128i:

static const uint8_t gK8[16] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

static inline foo()
{
    const __m128i K8 = _mm_loadu_si128((__m128i *)gK8);

    // ...
}

不使用 SSE 指令初始化 __m128i 向量是可能的,但这取决于编译器如何定义 __m128i。

对于 Microsoft Visual Studio 您可以定义下一个宏(它将 __m128i 定义为 char[16]):

template <class T> inline char GetChar(T value, size_t index)
{
    return ((char*)&value)[index];
}

#define AS_CHAR(a) char(a)

#define AS_2CHARS(a) \
    GetChar(int16_t(a), 0), GetChar(int16_t(a), 1)

#define AS_4CHARS(a) \
    GetChar(int32_t(a), 0), GetChar(int32_t(a), 1), \
    GetChar(int32_t(a), 2), GetChar(int32_t(a), 3)

#define _MM_SETR_EPI8(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af) \
    {AS_CHAR(a0), AS_CHAR(a1), AS_CHAR(a2), AS_CHAR(a3), \
     AS_CHAR(a4), AS_CHAR(a5), AS_CHAR(a6), AS_CHAR(a7), \
     AS_CHAR(a8), AS_CHAR(a9), AS_CHAR(aa), AS_CHAR(ab), \
     AS_CHAR(ac), AS_CHAR(ad), AS_CHAR(ae), AS_CHAR(af)}

#define _MM_SETR_EPI16(a0, a1, a2, a3, a4, a5, a6, a7) \
    {AS_2CHARS(a0), AS_2CHARS(a1), AS_2CHARS(a2), AS_2CHARS(a3), \
     AS_2CHARS(a4), AS_2CHARS(a5), AS_2CHARS(a6), AS_2CHARS(a7)}

#define _MM_SETR_EPI32(a0, a1, a2, a3) \
    {AS_4CHARS(a0), AS_4CHARS(a1), AS_4CHARS(a2), AS_4CHARS(a3)}       

对于 GCC 它将是(它将 __m128i 定义为 long long[2]):

#define CHAR_AS_LONGLONG(a) (((long long)a) & 0xFF)

#define SHORT_AS_LONGLONG(a) (((long long)a) & 0xFFFF)

#define INT_AS_LONGLONG(a) (((long long)a) & 0xFFFFFFFF)

#define LL_SETR_EPI8(a, b, c, d, e, f, g, h) \
    CHAR_AS_LONGLONG(a) | (CHAR_AS_LONGLONG(b) << 8) | \
    (CHAR_AS_LONGLONG(c) << 16) | (CHAR_AS_LONGLONG(d) << 24) | \
    (CHAR_AS_LONGLONG(e) << 32) | (CHAR_AS_LONGLONG(f) << 40) | \
    (CHAR_AS_LONGLONG(g) << 48) | (CHAR_AS_LONGLONG(h) << 56)

#define LL_SETR_EPI16(a, b, c, d) \
    SHORT_AS_LONGLONG(a) | (SHORT_AS_LONGLONG(b) << 16) | \
    (SHORT_AS_LONGLONG(c) << 32) | (SHORT_AS_LONGLONG(d) << 48)

#define LL_SETR_EPI32(a, b) \
    INT_AS_LONGLONG(a) | (INT_AS_LONGLONG(b) << 32)        

#define _MM_SETR_EPI8(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af) \
    {LL_SETR_EPI8(a0, a1, a2, a3, a4, a5, a6, a7), LL_SETR_EPI8(a8, a9, aa, ab, ac, ad, ae, af)}

#define _MM_SETR_EPI16(a0, a1, a2, a3, a4, a5, a6, a7) \
    {LL_SETR_EPI16(a0, a1, a2, a3), LL_SETR_EPI16(a4, a5, a6, a7)}

#define _MM_SETR_EPI32(a0, a1, a2, a3) \
    {LL_SETR_EPI32(a0, a1), LL_SETR_EPI32(a2, a3)}        

所以在你的代码中 __m128i 常量的初始化看起来像:

const __m128i K8 = _MM_SETR_EPI8(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
const __m128i K16 = _MM_SETR_EPI16(1, 2, 3, 4, 5, 6, 7, 8);
const __m128i K32 = _MM_SETR_EPI32(1, 2, 3, 4);

您可以使用联合。

union M128 {
   char[16] i8;
   __m128i i128;
};

const M128 k8 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

如果 M128 联合是在您使用循环的地方定义的,这应该没有性能开销(它将在循环开始时加载到内存中一次)。因为它包含一个__m128i类型的变量,M128继承了正确的对齐方式

void foo()
{
   M128 k8 = ...;
   // use k8.i128 in your for loop
}

如果在别处定义,那么在开始循环之前需要先拷贝到本地寄存器中,否则编译器可能无法优化。

void foo()
{
    __m128i tmp = k8.i128;
    // for loop here
}

这会将 k8 加载到 cpu 寄存器中,并在循环期间将其保存在那里,只要有足够的空闲寄存器来执行循环体。

根据您使用的编译器,这些联合可能已经定义(VS 定义),但编译器提供的定义可能不可移植。

你通常不需要这个。编译器非常擅长为使用相同常量的多个函数使用相同的存储空间。就像将相同字符串文字的多个实例合并为一个字符串常量一样,不同函数中相同 _mm_set* 的多个实例都将从相同的向量常量(或 用于 _mm_setzero_si128()_mm_set1_epi8(-1)).

使用 Godbolt 的二进制输出(反汇编)模式,您可以查看不同的函数是否从同一内存块加载。查看它添加的注释,它将 RIP 相对地址解析为绝对地址。

  • gcc:all identical constants share the same storage,无论它们是来自自动矢量化还是 _mm_set。 32B 常量不能与 16B 常量重叠,即使 16B 常量是 32B 的子集。

  • 叮当声:identical constants share storage。 16B 和 32B 常量不重叠,即使其中一个是另一个的子集。一些使用重复常量的函数使用 AVX2 vpbroadcastd 广播负载(在 Intel SnB 系列 CPU 上甚至不采用 ALU uop)。出于某种原因,它选择根据操作的元素大小而不是常量的重复性来执行此操作。请注意,clang 的 asm 输出会在每次使用时重复常量,但最终的二进制文件不会。

  • MSVC:identical constants share storage。与 gcc 所做的几乎相同。 (完整的 asm 输出很难通过;使用搜索。我只能通过让 main 找到 .exe 的路径来获得 asm,然后计算出使用 [= 生成的 asm 输出的路径17=], 和 运行 system("type .../foo.asm")).

编译器很擅长这个,因为这不是一个新问题。从编译器的早期开始,它就与字符串一起存在。

我还没有检查这是否适用于源文件(例如,对于在多个编译单元中使用的内联向量函数)。如果您仍然想要静态/全局向量常量,请参见下文:


似乎没有简单的 可移植的方法来静态初始化 static/global __m128。 C 编译器甚至不接受 _mm_set* 作为初始值设定项,因为它像函数一样工作。他们没有利用这样一个事实,即他们实际上可以通过它看到编译时间常数 16B

const __m128i K32 = _mm_setr_epi32(1, 2, 3, 4);   // Illegal in C
// C++: generates a constructor that copies from .rodata to the BSS

即使构造函数只需要 SSE1 或 SSE2,您也不需要它。这太糟糕了。 不要这样做。您最终支付了两次常量的内存成本。


Fabio 的 union 答案看起来是静态初始化向量常量的最佳可移植方式,但这意味着您必须访问 __m128i 联合成员。它可能有助于将相关常量彼此靠近分组(希望在同一缓存行中),即使它们被分散的函数使用。也有不可移植的方法来完成(例如,将相关常量放在它们自己的带有 GNU C __attribute__ ((section ("constants_for_task_A"))) 的 ELF 部分中)。希望可以将它们组合在 .rodata 部分(成为 .text 部分的一部分)。