【问题标题】:Is it possible to template the asm opcode in c++?是否可以在 C++ 中模板化 asm 操作码?
【发布时间】:2021-06-09 02:36:45
【问题描述】:

我想要这样的东西:

template <const char *op, int lane_select>
static int cmpGT64Sx2(V64x2 x, V64x2 y)
{
   int result;
   __asm__("movups %1,%%xmm6\n"
           "\tmovups %2,%%xmm7\n"
           // order swapped for AT&T style which has destination second.
           "\t " op " %%xmm7,%%xmm6\n"
           "\tpextrb %3, %%xmm6, %0"
           : "=r" (result) : "m" (x), "m" (y), "i" (lane_select*8) : "xmm6");
   return result;
}

显然它必须是一个模板,因为它必须在编译时知道。 lane_select 已经可以正常工作了,它是一个模板,但它是一个操作数。我希望 asm 中的 op 有所不同,例如 pcmpgtdpcmpgtq 等。如果有帮助:我总是想要某种形式的 x86 的 pcmpgt,只是最后一个字母发生变化。

编辑:

这是一个 valgrind 的测试用例,其中运行准确的指令非常重要,这样我们才能检查输出的定义性。

【问题讨论】:

  • 您意识到这将比仅使用内在函数更糟糕,对吧?像 _mm_loadu_si128((__m128i)&amp;x) 和 y,然后是 _mm_cmpgt_epi3264(您可以在编译时常量上使用 if() 进行选择)。然后_mm_movemask_ps_epi8 得到SIMD 比较结果的位图。或者,如果您只想要第 n 个成员,请在纯 C 中使用 memcpy 或其他别名安全加载到 int32_tint64_t 成员,从偏移到 xy
  • 另外,你在xmm7 上错过了一个clobber。最好只要求编译器将输入放在两个 "x" 输入中,而不是采用强制编译器将对象保存在内存中的内存源输入。
  • 如果您有一个类型为 int32_t 或 int64_t 的虚拟输出操作数,pcmpgt%z1几乎工作。 %z[op] 是 AT&T 大小后缀,b/w/l/q,所以它是 l 代表 dword,而不是 d。也许与 GAS .macro.if (sourceware.org/binutils/docs/as/If.html#If) 一起使用 "i" 输入作为模板参数(作为整数 char 值或简单的布尔值 0 或 1,当然不是指针)。
  • 这也更糟糕,因为正如 tadman 所说,创建错误非常容易。您的代码已经有了一个:就像我说的,缺少“xmm7”clobber。 (更好的是,只需使用两个 "=x" 虚拟输出让编译器选择寄存器,或者让它们输入和输入/输出,这样您就可以将 movups 留在模板之外。)
  • 是的,使用-march=nehalem-march=native -mno-avx。如果 AVX 可用,编译器在自动矢量化或内在函数时总是使用 VEX 编码。 (Nehalem 是 AVX 之前的最后一个 Intel uarch,所以它是 SSE4.2 + popcnt 等的一个很好的简写,调整选项比通用基线更新。或者只是 -msse4.1 当然启用您正在尝试的 ISA 扩展使用扩展,如果你坚持使用 pextrb 而不是 pmovmskb。)

标签: c++ templates assembly gcc inline-assembly


【解决方案1】:

这可以通过一些 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 native vector 样式并在将它们转换为v4si 之后执行x = x&gt;y,具体取决于您想要比较32 位还是64 位元素。如果 SSE4.2 不适用于 pcmpgtq,GCC 将实现运算符 >,但它可以使用多指令仿真。不过,这与内在函数之间没有其他根本区别。编译器不需要发出pcmpgtq 只是因为源包含_mm_cmpgt_epi64,例如如果 x 和 y 都是编译时常量,或者如果 y 已知为 LONG_MAX,那么它可以通过它进行常量传播,所以没有什么比它更大了。


使用内联汇编

只有 C 预处理器可以按照您希望的方式工作; asm 模板在编译时必须是字符串文字,而 AFAIK C++ 模板 constexpr 东西不能将变量的值字符串化并将其传递为实际的字符串文字。模板评估发生在解析之后。

我想出了一个有趣的技巧,让 GCC 使用 %p4 打印 dq 作为全局(或静态)变量的 asm 符号名称(参见 @ GCC 手册中的 987654323@。)像constexpr char d[] = {}; 这样的空数组在这里可能是一个不错的选择。无论如何,您都不能将字符串文字传递给模板参数。

(我还修复了您的内联 asm 语句中的低效率和错误:例如,让编译器选择寄存器,并要求 XMM regs 中的输入,而不是内存。您缺少“xmm7”clobber,但这个版本没有不需要任何破坏者。对于输入可能是编译时常量,或者一个在对齐内存中以便可以使用内存操作数或各种其他可能的优化的情况,这仍然比内在函数更糟糕。我本可以使用"xm"作为来源,但 clang 总是选择“m”。**https://gcc.gnu.org/wiki/DontUseInlineAsm**.)

如果您不希望它针对 valgrind 测试进行优化,即使不需要输出,也可以设置为 asm volatile 强制它运行。这就是您想要使用内联 asm 而不是内在函数或 GNU C 本机向量语法的唯一原因 (x &gt; 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]\n\t"   // %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 作为此文件中的全局范围名称为代价(!??),我们可以使用看起来像我们想要的指令的&lt;d, 2&gt;&lt;q, 0&gt; 模板参数。

请注意,在 x86 SIMD 术语中,“通道”是 AVX 或 AVX-512 向量的 128 位块。如vpermilps(32 位浮点元素的 In-Lane Permute)。

这使用 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

您可以对模板用户隐藏全局范围的变量,并让他们传递一个整数大小。我还修复了元素索引以考虑可变元素大小。

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]\n\t"  // 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 语句仅在与 "0"(x) 输入相同的寄存器中生成该类型的 "=x" 输出,则您可以使用 typedef int v4si __attribute__((vector_size(16))) 然后 v[element] 让 GCC 为您执行此操作.


没有全局范围的变量名称,使用 GAS .if / .else

我们可以很容易地让 GCC 将一个裸数字打印到 asm 模板中,例如用作a .if %[size] == 32 directive 的操作数。 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] == 32\n\t"         // note Godbolt hides directives; use binary mode to verify the assemble-time condition worked
         "pcmpgtd   %[src],%[dst]\n\t"  // SSE2
         ".else \n\t"
         "pcmpgtq   %[src],%[dst]\n\t"  // SSE4.2
         ".endif \n\t"
         "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
}

我还改为使用 SSE2 pmovmskb 来提取两个/所有元素比较结果,并使用标量内容来选择要查看的位。这是正交的,可以与任何其他人一起使用。内联后,它通常会更高效,允许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 使用 pcmpgtqshr 8。)

【讨论】:

  • 谢谢!对于 Valgrind 的这种用法,指令精确为 pcmpgt* 至关重要。我不能保证其他指令,如 vpcmpgt* 已被检测。在我的测试中,我看到编译器可能会生成不同的操作码。
  • @Eyal:如果你使用-mno-avx,GCC 和clang 肯定不会从内在函数发出vpcmpgt*。但就像我说的,它们理论上可以优化掉内在函数,例如如果已知y_mm_set1_epi32(INT_MAX)_mm_set1_epi64x(LLONG_MAX),那么没有什么比这更好了。
  • @Eyal:如果这只是为了测试一些关于 valgrind 的东西,那么内联 asm 肯定是有意义的。如果这是您手动矢量化以提高性能的真实程序的一部分,请使用内部函数。 (当你想在上面使用 valgrind 时,用 -O3 -march=native -mno-avx 编译它。或者如果 valgrind 不知道其他新指令,如 BMI2 shlx,则可能只是 -march=nehalem -mtune=native
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-25
  • 2016-10-03
  • 1970-01-01
  • 2011-04-30
  • 1970-01-01
  • 2022-01-21
  • 1970-01-01
相关资源
最近更新 更多