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

时间:2016-02-08 11:01:44

标签: c++ const sse simd sse2

我有很多函数使用相同的常量__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的情况下初始化这些常量吗?

4 个答案:

答案 0 :(得分:5)

可以在不使用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 [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);

答案 1 :(得分:4)

我建议将初始化数据全局定义为标量数据,然后将其本地加载到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);

    // ...
}

答案 2 :(得分: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确实如此),但编译器提供的定义可能不是可移植的。

答案 3 :(得分:2)

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

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

  • gcc:generate on the fly,无论是来自自动矢量化还是vpbroadcastd。 32B常数不能与16B常数重叠,即使16B常数是32B的子集。

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

  • MSVC:identical constants share storage。与gcc的功能几乎相同。 (完整的asm输出很难通过;使用搜索。我只能通过cl.exe -O2 /FAs找到.exe的路径来获取asm,然后计算出使用{{{ 1}},并运行system("type .../foo.asm"))。

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

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

似乎没有简单的可移植方式来静态初始化静态/全局__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联盟成员。它可能有助于将相关常量分组到彼此附近(希望在同一缓存行中),即使它们被分散的函数使用也是如此。还有一些非便携的方法可以完成(例如,将相关常量放在他们自己的ELF部分中identical constants share storage)。希望可以将它们组合在.rodata部分(它成为.text部分的一部分)。