是否可以在C++中模板化asm操作码?
我想要这样的东西:
template <const char *op, int lane_select>
static int cmpGT64Sx2(V64x2 x, V64x2 y)
{
int result;
__asm__("movups %1,%%xmm6n"
"tmovups %2,%%xmm7n"
// order swapped for AT&T style which has destination second.
"t " op " %%xmm7,%%xmm6n"
"tpextrb %3, %%xmm6, %0"
: "=r" (result) : "m" (x), "m" (y), "i" (lane_select*8) : "xmm6");
return result;
}
显然它必须是一个模板,因为它必须在编译时已知。该lane_select 工作正常,它是一个模板,但它是一个操作数。我希望op
asm 中的 是不同的,例如pcmpgtd
或pcmpgtq
等。如果有帮助:我总是想要某种形式的 x86 pcmpgt
,只需要更改最后一个字母。
编辑:
这是 valgrind 的一个测试用例,其中运行确切的指令非常重要,以便我们可以检查输出的定义性。
回答
这是可能的一些 asm 黑客,但通常你最好使用内在函数,如下所示:https :
//gcc.gnu.org/wiki/DontUseInlineAsm
#include <immintrin.h>
template<int size, int element>
int foo(__m128i x, __m128i y) {
if (size==32) // if constexpr if you want to be C++17 fancy
x = _mm_cmpgt_epi32(x, y);
else
x = _mm_cmpgt_epi64(x, y); // needs -msse4.2 or -march=native or whatever
return x[element]; // GNU C extension to index vectors with [].
// GCC defines __m128i as a vector of two long long
// cast to typedef int v4si __attribute__((vector_size(16)))
// or use _mm_extract_epi8 or whatever with element*size/8
// if you want to access one of 4 dword elements.
}
int test(__m128i x, __m128i y) {
return foo<32, 0>(x, y);
}
// compiles to pcmpgtd %xmm1, %xmm0 ; movq %xmm0, %rax ; ret
您甚至可以使用完整的GNU C 原生矢量样式并x = x>y
在将它们转换为v4si
或不转换后执行,具体取决于您想要 32 位元素还是 64 位元素比较。GCC 将实现运算符 > 但是它可以,如果 SSE4.2 不可用于 pcmpgtq,则使用多指令仿真。不过,这与内在函数之间没有其他根本区别。编译器不需要pcmpgtq
仅仅因为源包含就发出_mm_cmpgt_epi64
,例如,如果 x 和 y 都是编译时常量,或者如果 y 已知是 LONG_MAX,那么它可以通过它进行常量传播,所以没有什么可以大于它。
使用内联汇编
只有 C 预处理器才能按照您希望的方式工作;asm 模板在编译时必须是字符串文字,而 AFAIK C++ 模板 constexpr 东西不能字符串化并将变量的值传递到实际的字符串文字中。模板评估发生在解析之后。
我想出了一个有趣的 hack,它让 GCC 打印d
或q
作为全局(或静态)变量的 asm 符号名称,使用%p4
(请参阅GCC 手册中的操作数修饰符。)空数组 likeconstexpr char d[] = {};
在这里可能是一个不错的选择。无论如何,您不能将字符串文字传递给模板参数。
(我还修复了内联 asm 语句中的低效率和错误:例如让编译器选择寄存器,并要求 XMM regs 中的输入,而不是内存。您缺少“xmm7”clobber,但此版本不需要任何clobbers。在输入可能是编译时常量的情况下,这仍然比内在函数更糟糕,或者在对齐的内存中,因此可以使用内存操作数或其他各种可能的优化。我可以用作"xm"
源,但 clang 会始终选择“m”。** https://gcc.gnu.org/wiki/DontUseInlineAsm**。)
如果您需要它不针对 valgrind 测试进行优化,asm volatile
即使不需要输出,也可以强制它运行。这是您想要使用内联 asm 而不是内在函数或 GNU C 本机向量语法 ( x > y
)
typedef long long V64x2 __attribute__((vector_size(16), may_alias));
// or #include <immintrin.h> and use __m128i which is defined the same way
static constexpr char q[0] asm("q") = {}; // override asm symbol name which gets mangled for const or constexpr
static constexpr char d[0] asm("d") = {};
template <const char *op, int element_select>
static int cmpGT64Sx2(V64x2 x, V64x2 y)
{
int result;
__asm__(
// AT&T style has destination second.
"pcmpgt%p[op] %[src],%[dst]nt" // %p4 - print the bare name, not $d or $q
"pextrb %3, %[dst], %0"
: "=r" (result), [dst]"+x"(x)
: [src]"x"(y), "i" (element_select*8),
[op]"i"(op) // address as an immediate = symbol name
: /* no clobbers */);
return result;
}
int gt64(V64x2 x, V64x2 y) {
return cmpGT64Sx2<q, 1>(x,y);
}
int gt32(V64x2 x, V64x2 y) {
return cmpGT64Sx2<d, 1>(x,y);
}
因此,以在此文件中拥有d
和q
作为全局范围名称为代价(!??),我们可以使用看起来像我们想要的指令的<d, 2>
或<q, 0>
模板参数。
请注意,在 x86 SIMD 术语中,“通道”是 AVX 或 AVX-512 向量的 128 位块。就像在vpermilps(32 位浮点元素的车道内置换)中一样。
这使用 GCC10 -O3 ( https://godbolt.org/z/ovxWd8 )编译为以下 asm
gt64(long long __vector(2), long long __vector(2)):
pcmpgtq %xmm1,%xmm0
pextrb $8, %xmm0, %eax
ret
gt32(long long __vector(2), long long __vector(2)):
pcmpgtd %xmm1,%xmm0
pextrb $8, %xmm0, %eax // This is actually element 2 of 4, not 1, because your scale doesn't account for the size.
ret
您可以对模板用户隐藏全局范围的变量,并让他们传递一个整数 size。我还修复了元素索引以考虑可变元素大小。
static constexpr char q[0] asm("q") = {}; // override asm symbol name which gets mangled for const or constexpr
static constexpr char d[0] asm("d") = {};
template <int size, int element_select>
static int cmpGT64Sx2_new(V64x2 x, V64x2 y)
{
//static constexpr char dd[0] asm("d") = {}; // nope, asm symbol name overrides don't work on local-scope static vars
constexpr int bytepos = size/8 * element_select;
constexpr const char *op = (size==32) ? d : q;
// maybe static_assert( size == 32 || size == 64 )
int result;
__asm__(
// AT&T style has destination second.
"pcmpgt%p[op] %[src],%[dst]nt" // SSE2 or SSE4.2
"pextrb %[byte], %[dst], %0" // SSE4.1
: "=r" (result), [dst]"+x"(x)
: [src]"x"(y), [byte]"i" (bytepos),
[op]"i"(op) // address as an immediate = symbol name
: /* no clobbers */);
return result;
}
// Note *not* referencing d or q static vars, but the template is
int gt64_new(V64x2 x, V64x2 y) {
return cmpGT64Sx2_new<64, 1>(x,y);
}
int gt32_new(V64x2 x, V64x2 y) {
return cmpGT64Sx2_new<32, 1>(x,y);
}
这也像我们想要的那样编译,例如
gt32_new(long long __vector(2), long long __vector(2)):
pcmpgtd %xmm1,%xmm0
pextrb $4, %xmm0, %eax # note the correct element 1 position
ret
顺便说一句,如果您的 asm 语句只是在与输入相同的寄存器中生成该类型的输出,则您可以使用typedef int v4si __attribute__((vector_size(16)))
然后v[element]
让 GCC 为您完成。"=x"
"0"(x)
没有全局范围的变量名,使用 GAS .if
/.else
我们可以很容易地得到GCC打印裸数到ASM模板,例如用作操作数以一个.if %[size] == 32
指令。GNU 汇编器具有一些条件汇编功能,因此我们只需让 GCC 为其提供正确的文本输入即可使用它。C++ 方面的黑客攻击要少得多,但源代码不那么紧凑。如果您想比较它而不是尺寸数字,您的模板参数可以是一个'd'
或'q'
尺寸代码字符。
template <int size, int element_select>
static int cmpGT64Sx2_mask(V64x2 x, V64x2 y)
{
constexpr int bytepos = size/8 * element_select;
unsigned int result;
__asm__(
// AT&T style has destination second.
".if %c[opsize] == 32nt" // note Godbolt hides directives; use binary mode to verify the assemble-time condition worked
"pcmpgtd %[src],%[dst]nt" // SSE2
".else nt"
"pcmpgtq %[src],%[dst]nt" // SSE4.2
".endif nt"
"pmovmskb %[dst], %0"
: "=r" (result), [dst]"+x"(x)
: [src]"x"(y), [opsize]"i"(size) // address as an immediate = symbol name
: /* no clobbers */);
return (result >> bytepos) & 1; // can just be TEST when branching on it
}
我还改为使用 SSE2pmovmskb
来提取两个/所有元素比较结果,并使用标量来选择要查看的位。这是正交的,可以与任何其他人一起使用。内联后,它通常会更有效率,允许test $imm32, %eax
. (pmovmskb 比 pextrb 便宜,它让整个过程只需要 pcmpgtd 版本的 SSE2)。
编译器的asm输出看起来像
.if 64 == 32
pcmpgtd %xmm1,%xmm0
.else
pcmpgtq %xmm1,%xmm0
.endif
pmovmskb %xmm0, %eax
为了确保做到了我们想要的,我们可以组装成二进制文件并查看反汇编(https://godbolt.org/z/5zGdfv):
gt32_mask(long long __vector(2), long long __vector(2)):
pcmpgtd %xmm1,%xmm0
pmovmskb %xmm0,%eax
shr $0x4,%eax
and $0x1,%eax
(和 gt64_mask 使用pcmpgtq
和shr
8。)
- @Eyal: if this is just for testing something about valgrind, sure inline asm makes sense. If this is part of a real program that you're manually vectorizing for performance, use intrinsics. (And compile it with `-O3 -march=native -mno-avx` when you want to use valgrind on it. Or maybe just `-march=nehalem -mtune=native` if valgrind doesn't know about other new instructions like BMI2 `shlx`)