ARM 内联汇编代码有错误 "impossible constraint in asm"

ARM inline assembly code with error "impossible constraint in asm"

我正在尝试优化以下代码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 包含 return 值但 return 类型是一个复杂的结构,它是如何工作的,因为 r0 只是一个 32 位?注册。

原c代码我在post这里:

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

你的内联汇编代码有一些错误:

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

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

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

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 所述:

If the selected floating-point hardware includes the NEON extension (e.g. -mfpu=‘neon’), note that floating-point operations are not generated by GCC's auto-vectorization pass unless -funsafe-math-optimizations is also specified. This is because NEON hardware does not fully implement the IEEE 754 standard for floating-point arithmetic (in particular denormal values are treated as zero), so the use of NEON instructions may lead to a loss of precision.

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

#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,这可能很棘手,但比使用内联汇编正确处理所有内容要容易得多。