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

问题描述 投票:1回答:2

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

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
   }
}

当“启用”为真时,我想创建以下功能:

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 ++ Template + Conditional Compile”来解决我的问题。代码如下。但它没有用。无论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()那样编写代码。有谁知道如何解决我的问题?非常感谢。

c++ templates assembly condition
2个回答
3
投票

如果对前半部分的所有实时寄存器使用C临时值并输出操作数,这些寄存器与下半部分的输入约束对齐,则应该能够将其拆分为内联asm而不会造成任何性能损失,尤其是在使用特定内存输入的情况下/输出约束而不是一个全能的"memory" clobber。但它会变得更复杂。


这显然不起作用,因为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"[enable] "i" (c_var)将替换为.if 0.if 1到inline-asm模板,正是我们需要使.if / .endif在汇编时工作。

完整示例:

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 *);

compiles with GCC and Clang on the Godbolt compiler explorer

使用gcc,您只能获得编译器的.s输出,因此您必须关闭一些常用的编译器 - 资源管理器过滤器并查看指令。所有3个vadd.f32指令都在false版本中,但其中一个被.if 0 / .endif包围。

但clang的内置汇编程序在内部处理汇编程序指令,然后在请求输出时将其转换回asm。 (通常clang / LLVM直接进入机器代码,不像gcc总是运行一个单独的汇编程序)。

为了清楚起见,这适用于gcc和clang,但是它更容易在带有铿锵声的Godbolt上看到它。 (因为Godbolt没有“二进制”模式,实际上组装然后拆解,除了x86)。 clang输出为false版本

 ...

    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]

 ... 

请注意,clang为原始指针选择了与用于内存操作数的GP寄存器相同的GP寄存器。 (gcc似乎选择[sp]用于src_mem,但是在寻址模式下手动使用的指针输入有不同的reg)。如果你没有强制它在寄存器中有指针,它可能使用SP相对寻址模式和向量加载的偏移量,可能利用ARM寻址模式。

如果你真的不想修改asm中的指针(例如使用后增量寻址模式),那么"r"仅输入操作数最有意义。如果我们离开了printf循环,编译器将在asm之后再次需要dst,因此它仍然可以从寄存器中获益。 "+r"(dst_addr)输入强制编译器假定该寄存器不再可用作dst的副本。无论如何,gcc总是复制寄存器,即使它以后不需要它,无论我是使它成为"r"还是"+r",所以这很奇怪。

使用(虚拟)存储器输入/输出意味着我们可以删除volatile,因此编译器可以正常优化它作为其输入的纯函数。 (如果结果未使用,请将其优化掉。)

希望这不是更糟糕的代码 - 与"memory" clobber。但是如果你只使用"=m""m"内存操作数可能会更好,并且根本没有在寄存器中请求指针。 (但是,如果您要使用内联asm循环遍历数组,那将无济于事。)

另见Looping over arrays with inline assembly


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不会简单地计算最终字符串,其值为输出字符串,因为输入是常量。也许是关于精度的一些数学规则阻止它在编译时这样做,因为结果可能与实际的目标HW平台FPU略有不同?即使用一些快速数学开关,它可能会完全丢弃该代码,只生成一个输出字符串......

但是我猜你的代码实际上并不适合你所做的“MCVE”,并且测试值会被输入到你正在测试的某个实际函数中,或类似的东西。

无论如何,如果你正在进行性能优化,你可能宁愿完全避免内联汇编而是使用内在函数,因为这样可以让编译器更好地分配寄存器并优化计算周围的代码(我没有准确地跟踪它,但我认为在godbolt中这个实验的最后一个版本比使用内联汇编的原始版本更简单/更简单2-4个指令。

另外,您将避免像示例代码那样不正确的asm约束,如果您经常修改内联代码,那么要正确获取并且要保持纯PITA总是很棘手。

© www.soinside.com 2019 - 2024. All rights reserved.