ARM内联汇编代码,错误" asm中的不可能约束"

时间:2016-09-22 09:59:22

标签: gcc assembly arm inline neon

我正在尝试优化以下代码complex.cpp:

typedef struct {
    float re;
    float im;
} dcmplx;

dcmplx ComplexConv(int len, dcmplx *hat, dcmplx *buf)
{
    int    i;
    dcmplx    z, xout;

    xout.re = xout.im = 0.0;
    asm volatile (
    "movs r3, #0\n\t"
    ".loop:\n\t"
    "vldr s11, [%[hat], #4]\n\t"
    "vldr s13, [%[hat]]\n\t"
    "vneg.f32 s11, s11\n\t"
    "vldr s15, [%[buf], #4]\n\t"
    "vldr s12, [%[buf]]\n\t"
    "vmul.f32 s14, s15, s13\n\t"
    "vmul.f32 s15, s11, s15\n\t"
    "adds %[hat], #8\n\t"
    "vmla.f32 s14, s11, s12\n\t"
    "vnmls.f32 s15, s12, s13\n\t"
    "adds %[buf], #8\n\t"
    "vadd.f32 s1, s1, s14\n\t"
    "vadd.f32 s0, s0, s15\n\t"
    "adds r3, r3, #1\n\t"
    "cmp r3, r0\n\t"
    "bne .loop\n\t"
    : "=r"(xout)
    : [hat]"r"(hat),[buf]"r"(buf) 
    : "s0","cc"
    );
    return xout;
}

使用" arm-linux-gnueabihf-g ++ -c complex.cpp -o complex.o -mfpu = neon"编译时, 我收到了以下错误:' asm'中的不可能约束。

当我评论" = r"(xout)时,编译没有抱怨,但我怎样才能得到寄存器的结果' s0'进入xout?

此外,如果r0包含返回值但返回类型是一个复杂的结构,它是如何工作的,因为r0只是一个32位?注册

我在这里发布的原始c代码:

dcmplx ComplexConv(int len, dcmplx *hat, dcmplx *buf)
{
    int    i;
    dcmplx    z, xout;
    xout.re = xout.im = 0.0;
    for(int i = 0; i < len; i++) {
        z = BI_dcmul(BI_dconjg(hat[i]),buf[i]);
        xout = BI_dcadd(xout,z);
    }
    return xout;
}
dcmplx BI_dcmul(dcmplx x, dcmplx y)
{
    dcmplx    z;
    z.re = x.re * y.re - x.im * y.im;
    z.im = x.im * y.re + x.re * y.im;
    return z;
}
dcmplx BI_dconjg(dcmplx x)
{
    dcmplx    y;
    y.re = x.re;
    y.im = -x.im;
    return y;
}
dcmplx BI_dcadd(dcmplx x, dcmplx y)
{
    dcmplx    z;
    z.re = x.re + y.re;
    z.im = x.im + y.im;
    return z;
}

1 个答案:

答案 0 :(得分:6)

您的内联汇编代码会出现一些错误:

  • 它尝试使用64位结构作为具有32位输出寄存器("=r")约束的操作数。这就是给你错误的原因。
  • 它不会在任何地方使用该输出操作数
  • 它不告诉编译器实际输出的位置(S0 / S1)
  • 它不告诉编译器len应该是输入
  • 它没有告诉编译器,它破坏了许多寄存器R3,S11,S12,S13,S14,S14。
  • 它使用标签.loop,不必要地阻止编译器在多个位置内联代码。
  • 它实际上看起来并不等同于您展示的C ++代码,而是计算其他代码。

我不打算解释如何解决所有这些错误,因为你shouldn't be using inline assembly。您可以用C ++编写代码,让编译器进行矢量化。

例如,编译以下代码,相当于您的示例C ++代码,使用GCC 4.9和-O3 -funsafe-math-optimizations选项:

dcmplx ComplexConv(int len, dcmplx *hat, dcmplx *buf)
{
    int    i;
    dcmplx xout;
    xout.re = xout.im = 0.0;
    for (i = 0; i < len; i++) {
        xout.re += hat[i].re * buf[i].re + hat[i].im * buf[i].im;
        xout.im += hat[i].re * buf[i].im - hat[i].im * buf[i].re;
    }
    return xout;
}

生成以下程序集作为其内循环:

.L97:
    add lr, lr, #1
    cmp ip, lr
    vld2.32 {d20-d23}, [r5]!
    vld2.32 {d24-d27}, [r4]!
    vmul.f32    q15, q12, q10
    vmul.f32    q14, q13, q10
    vmla.f32    q15, q13, q11
    vmls.f32    q14, q12, q11
    vadd.f32    q9, q9, q15
    vadd.f32    q8, q8, q14
    bhi .L97

根据你的内联汇编代码,如果你试图自己进行矢量化,编译器的生成可能比你想象的更好。

-funsafe-math-optimizations是必要的,因为NEON指令不完全符合IEEE 754标准。正如GCC documentation所述:

  

如果选定的浮点硬件包含NEON扩展名   (例如-mfpu=‘neon’),请注意浮点运算不是   由GCC的自动矢量化传递产生,除非   还指定了-funsafe-math-optimizations。这是因为NEON硬件没有完全实现IEEE 754标准   浮点运算(特别是非正规值被处理   为零),因此使用NEON指令可能会导致丢失   精度。

我还应该注意,如果不滚动自己的复杂类型,编译器生成的代码几乎与上面的代码一样好,如下例所示:

#include <complex>
typedef std::complex<float> complex;
complex ComplexConv_std(int len, complex *hat, complex *buf)
{
    int    i;
    complex xout(0.0f, 0.0f); 
    for (i = 0; i < len; i++) {
        xout += std::conj(hat[i]) * buf[i];
    }
    return xout;
}

然而,使用您自己的类型的一个好处是,您可以改进代码编译器生成,对您声明struct dcmplx的方式做一个小的改动:

typedef struct {
    float re;
    float im;
} __attribute__((aligned(8)) dcmplx;

通过说它需要8字节(64位)对齐,这允许编译器跳过检查以查看它是否适当对齐,然后再回到较慢的标量实现上。

现在,假设你可以说你对GCC如何矢量化你的代码感到不满意,并认为你可以做得更好。这是否有理由使用内联汇编?不,接下来要尝试的是ARM NEON intrinsics。使用内在函数就像普通的C ++编程一样,您不必担心需要遵循的一系列特殊规则。例如,这是我如何将上面的矢量化程序集转换为使用内在函数的未经测试的代码:

#include <assert.h>
#include <arm_neon.h>
dcmplx ComplexConv(int len, dcmplx *hat, dcmplx *buf)
{
    int    i;
    dcmplx xout;

    /* everything needs to be suitably aligned */
    assert(len % 4 == 0);
    assert(((unsigned) hat % 8) == 0);
    assert(((unsigned) buf % 8) == 0);

    float32x4_t re, im;
    for (i = 0; i < len; i += 4) {
        float32x4x2_t h = vld2q_f32(&hat[i].re);
        float32x4x2_t b = vld2q_f32(&buf[i].re);
        re = vaddq_f32(re, vmlaq_f32(vmulq_f32(h.val[0], b.val[0]),
                                     b.val[1], h.val[1]));
        im = vaddq_f32(im, vmlsq_f32(vmulq_f32(h.val[1], b.val[1]),
                                     b.val[0], h.val[0]));
    }
    float32x2_t re_tmp = vadd_f32(vget_low_f32(re), vget_high_f32(re));
    float32x2_t im_tmp = vadd_f32(vget_low_f32(im), vget_high_f32(im));
    xout.re = vget_lane_f32(vpadd_f32(re_tmp, re_tmp), 0);
    xout.im = vget_lane_f32(vpadd_f32(im_tmp, im_tmp), 0);
    return xout;
}

最后,如果这不够好并且您需要调整每一点性能,那么使用内联汇编仍然不是一个好主意。相反,你最后的选择应该是使用常规装配。由于您在汇编中重写了大部分函数,​​因此您可以在汇编中完全编写它。这意味着您不必担心告诉编译器您在内联汇编中所做的一切。您只需要符合ARM ABI,这可能非常棘手,但比使用内联汇编使一切正确更容易。