是否可以在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 工作正常,它是一个模板,但它是一个操作数。我希望opasm 中的 是不同的,例如pcmpgtdpcmpgtq等。如果有帮助:我总是想要某种形式的 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 打印dq作为全局(或静态)变量的 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);
}

因此,以在此文件中拥有dq作为全局范围名称为代价(!??),我们可以使用看起来像我们想要的指令的<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 使用pcmpgtqshr8。)

  • @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`)

以上是是否可以在C++中模板化asm操作码?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>