如何使用c ++模板有条件地编译asm代码?

时间:2019-02-15 10:56:27

标签: c++ templates assembly conditional-statements

有一个名为“ Enable”的布尔变量,当“ Enable”为假时,我想创建以下函数:

void test_false()
{
   float dst[4] = {1.0, 1.0, 1.0, 1.0};
   float src[4] = {1.0, 2.0, 3.0, 4.0};
   float * dst_addr = dst;
   float * src_addr = src;


   asm volatile (
                 "vld1.32    {q0}, [%[src]]  \n"
                 "vld1.32    {q1}, [%[dst]]  \n"
                 "vadd.f32   q0, q0, q1      \n"
                 "vadd.f32   q0, q0, q1      \n"
                 "vst1.32    {q0}, [%[dst]]  \n"
                 :[src]"+r"(src_addr),
                 [dst]"+r"(dst_addr)
                 :
                 : "q0", "q1", "q2", "q3", "memory"
                 );

   for (int i = 0; i < 4; i++)
   {
       printf("%f, ", dst[i]);//0.0  0.0  0.0  0.0
   }
}

当“启用”为true时,我想创建以下函数:

void test_true()
{
   float dst[4] = {1.0, 1.0, 1.0, 1.0};
   float src[4] = {1.0, 2.0, 3.0, 4.0};
   float * dst_addr = dst;
   float * src_addr = src;


   asm volatile (
                 "vld1.32    {q0}, [%[src]]  \n"
                 "vld1.32    {q1}, [%[dst]]  \n"
                 "vadd.f32   q0, q0, q1      \n"
                 "vadd.f32   q0, q0, q1      \n"
                 "vadd.f32   q0, q0, q1      \n" //Only here is different from test_false()
                 "vst1.32    {q0}, [%[dst]]  \n"
                 :[src]"+r"(src_addr),
                 [dst]"+r"(dst_addr)
                 :
                 : "q0", "q1", "q2", "q3", "memory"
                 );

   for (int i = 0; i < 4; i++)
   {
       printf("%f, ", dst[i]);//0.0  0.0  0.0  0.0
   }
}

但是我不想保存两个代码副本,因为它们大多数都是相同的。我想使用“ c ++模板+条件编译”来解决我的问题。代码如下。但这没有用。无论Enable是true还是false,编译器都会创建与test_true()相同的代码。

template<bool Enable>
void test_tmp()
{
   float dst[4] = {1.0, 1.0, 1.0, 1.0};
   float src[4] = {1.0, 2.0, 3.0, 4.0};
   float * dst_addr = dst;
   float * src_addr = src;

    if (Enable)
    {
        #define FUSE_
    }

   asm volatile (
                 "vld1.32    {q0}, [%[src]]  \n"
                 "vld1.32    {q1}, [%[dst]]  \n"
                 "vadd.f32   q0, q0, q1          \n"
                 "vadd.f32   q0, q0, q1          \n"

                 #ifdef FUSE_
                 "vadd.f32   q0, q0, q1          \n"
                 #endif

                 "vst1.32    {q0}, [%[dst]]  \n"
                 :[src]"+r"(src_addr),
                 [dst]"+r"(dst_addr)
                 :
                 : "q0", "q1", "q2", "q3", "memory"
                 );



   for (int i = 0; i < 4; i++)
   {
       printf("%f, ", dst[i]);//0.0  0.0  0.0  0.0
   }

   #undef FUSE_
}


template void test_tmp<true>();
template void test_tmp<false>();

似乎无法编写类似test_tmp()函数的代码。有人知道如何解决我的问题吗?非常感谢。

2 个答案:

答案 0 :(得分:3)

如果在上半部分的所有活动寄存器中使用C临时变量并输出操作数,并在第二半部分中使用输入约束,则应该能够将其拆分为内联组件,而不会造成任何性能损失,特别是如果使用特定的内存输入/输出约束,而不是全面的"memory"破坏者。但这会变得更加复杂。


这显然是行不通的,因为C预处理程序在之前运行,C ++编译器甚至会查看if()语句。

if (Enable) {
    #define FUSE_    // always defined, regardless of Enable
}

但是GNU汇编程序有自己的宏/条件汇编指令,例如.if,它在将文本替换为asm()模板后,对编译器发出的asm进行操作,包括立即输入的实际数值操作数。

使用bool作为an assembler .if directive的输入操作数

使用"i" (Enable)输入约束。通常,将其的%0%[enable]扩展为#0#1,因为这是立即打印ARM的方法。但是GCC具有%c0 / %c[enable]修饰符,该修饰符将打印不带标点的常量。 (它是documented for x86,但是对于ARM以及大概所有其他体系结构都以相同的方式工作。有关ARM / AArch64操作数修饰符的文档正在开发中;我一直在收到有关此内容的电子邮件...)

".if %c[enable] \n\t"的{​​{1}}将替换为[enable] "i" (c_var).if 0 到内联自动贩卖机模板中,正是我们需要制作的{{ 1}} / .if 1在组装时工作。

完整示例:

.if

compiles with GCC and Clang on the Godbolt compiler explorer

使用gcc,您只能获得编译器的.endif输出,因此您必须关闭一些常用的编译器-浏览器过滤器并仔细查看指令。所有3条template<bool Enable> void test_tmp(float dst[4]) { //float dst[4] = {1.0, 1.0, 1.0, 1.0}; // static const // non-static-const so we can see the memory clobber vs. dummy src stop this from optimizing away init of src[] on the stack float src[4] = {1.0, 2.0, 3.0, 4.0}; float * dst_addr = dst; const float * src_addr = src; asm ( "vld1.32 {q1}, [%[dst]] @ dummy dst = %[dummy_memdst]\n" // hopefully they pick the same regs? "vld1.32 {q0}, [%[src]] @ dummy src = %[dummy_memsrc]\n" "vadd.f32 q0, q0, q1 \n" // TODO: optimize to q1+q1 first, without a dep on src "vadd.f32 q0, q0, q1 \n" // allowing q0+=q1 and q1+=q1 in parallel if we need q0 += 3*q1 // #ifdef FUSE_ ".if %c[enable]\n" // %c modifier: print constant without punctuation, same as documented for x86 "vadd.f32 q0, q0, q1 \n" ".endif \n" // #endif "vst1.32 {q0}, [%[dst]] \n" : [dummy_memdst] "+m" (*(float(*)[4])dst_addr) : [src]"r"(src_addr), [dst]"r"(dst_addr), [enable]"i"(Enable) , [dummy_memsrc] "m" (*(const float(*)[4])src_addr) : "q0", "q1", "q2", "q3" //, "memory" ); /* for (int i = 0; i < 4; i++) { printf("%f, ", dst[i]);//0.0 0.0 0.0 0.0 } */ } float dst[4] = {1.0, 1.0, 1.0, 1.0}; template void test_tmp<true>(float *); template void test_tmp<false>(float *); 指令都在.s版本中,但是其中一条被vadd.f32 / false包围。

但是clang的内置汇编器在内部处理汇编器指令,如果需要该输出,则将其转换回asm。 (通常,clang / LLVM直接使用机器代码,不像gcc总是运行单独的汇编程序。)

只需清楚一点,它就可以与gcc clang一起使用,但是在带clang的Godbolt上更容易看到它。 (因为除了x86,Godbolt没有“ binary”模式,该模式实际上会进行组装然后反汇编)。 .if 0版本的语音输出

.endif

请注意,clang为原始指针选择了与用于内存操作数相同的GP寄存器。 (gcc似乎为src_mem选择false,但是为您在寻址模式下手动使用的指针输入设置了不同的reg)。如果您没有强迫它将指针放在寄存器中,它可能会使用相对于SP的寻址模式,并为矢量加载添加了偏移量,从而可能利用了ARM寻址模式。

如果您真的不打算修改asm内部的指针(例如,使用后递增寻址模式),则 ... vld1.32 {d2, d3}, [r0] @ dummy dst = [r0] vld1.32 {d0, d1}, [r1] @ dummy src = [r1] vadd.f32 q0, q0, q1 vadd.f32 q0, q0, q1 vst1.32 {d0, d1}, [r0] ... 仅输入操作数最有意义。如果我们留在[sp]循环中,则编译器将在汇编后再次需要"r",因此将其保留在寄存器中将受益匪浅。 printf输入会强制编译器假定该寄存器不再可用作dst的副本。无论如何,即使我以后将它设为"+r"(dst_addr)dst,gcc 总是都会复制寄存器,即使以后不再需要它了,也很奇怪。

使用(虚拟)内存输入/输出意味着我们可以删除"r",因此编译器可以将其正常优化为输入的纯函数。 (如果结果未使用,请对其进行优化。)

希望这并不比"+r"更强大。但是,如果您只是使用 volatile"memory"内存操作数,并且根本不要求寄存器中提供指针,那可能会更好。 (不过,如果您要使用内联asm遍历数组,则无济于事。)

另请参阅Looping over arrays with inline assembly

答案 1 :(得分:1)

几年来我一直没有进行ARM汇编,而且我从来没有真正地去认真地学习过GCC内联汇编,但是我认为可以使用内在函数像这样重写您的代码:

#include <cstdio>
#include <arm_neon.h>

template<bool Enable>
void test_tmp()
{
    const float32x4_t src = {1.0, 2.0, 3.0, 4.0};
    const float32x4_t src2 = {1.0, 1.0, 1.0, 1.0};
    float32x4_t z;

    z = vaddq_f32(src, src2);
    z = vaddq_f32(z, src2);
    if (Enable) z = vaddq_f32(z, src2);
    float result[4];
    vst1q_f32(result, z);
    for (int i = 0; i < 4; i++)
    {
        printf("%f, ", result[i]);//0.0  0.0  0.0  0.0
    }
}

template void test_tmp<true>();
template void test_tmp<false>();

您可以在https://godbolt.org/z/Fg7Tci

上实时查看生成的机器代码和玩具。

使用ARM gcc8.2和命令行选项“ -O3 -mfloat-abi = softfp -mfpu = neon”编译时,“ true”变量是:

void test_tmp<true>():
        vmov.f32        q9, #1.0e+0  @ v4sf
        vldr    d16, .L6
        vldr    d17, .L6+8
        # and the FALSE variant has one less vadd.f32 in this part
        vadd.f32        q8, q8, q9
        vadd.f32        q8, q8, q9
        vadd.f32        q8, q8, q9
        push    {r4, r5, r6, lr}
        sub     sp, sp, #16
        vst1.32 {d16-d17}, [sp:64]
        mov     r4, sp
        ldr     r5, .L6+16
        add     r6, sp, #16
.L2:
        vldmia.32       r4!, {s15}
        vcvt.f64.f32    d16, s15
        mov     r0, r5
        vmov    r2, r3, d16
        bl      printf
        cmp     r4, r6
        bne     .L2
        add     sp, sp, #16
        pop     {r4, r5, r6, pc}

.L6:
        .word   1065353216
        .word   1073741824
        .word   1077936128
        .word   1082130432
        .word   .LC0

.LC0:
        .ascii  "%f, \000"

这仍然让我感到非常困惑,为什么gcc不能简单地计算最终字符串,其值作为输出的字符串,因为输入是恒定的。也许这是关于精度的一些数学规则,以防止在编译时执行该操作,因为结果可能与实际目标硬件平台FPU略有不同?即使用一些快速操作开关,它可能会完全删除该代码,只产生一个输出字符串...

但是我想您的代码实际上并不是您正在执行的操作的正确“ MCVE”,并且测试值将被馈送到您正在测试的某些实际函数中,或者类似的东西。

无论如何,如果您正在进行性能优化,则可能应该完全避免内联汇编,而应使用内在函数,因为这可以使编译器更好地分配寄存器并优化计算代码(I不能精确跟踪它,但我认为该实验的最新版本比使用内联汇编的原始指令短/简单了2-4条指令。

此外,您将避免像示例代码一样遇到错误的asm约束,如果您经常修改内联代码,那么这些约束总是很难获得正确的信息,并且难以维护纯PITA。