如何在不使用任何 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
部分的一部分)。
我有很多函数使用相同的常量 __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
部分的一部分)。