Ale*_*Xie 1 assembly gcc 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;
}
Run Code Online (Sandbox Code Playgroud)
当它用"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;
}
Run Code Online (Sandbox Code Playgroud)
您的内联汇编代码会出现许多错误:
"=r")约束的操作数.这就是给你错误的原因.len应该是一个输入.loop,不必要地阻止编译器在多个位置内联代码.我不打算解释如何解决所有这些错误,因为你不应该使用内联汇编.您可以用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;
}
Run Code Online (Sandbox Code Playgroud)
生成以下程序集作为其内循环:
.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
Run Code Online (Sandbox Code Playgroud)
根据您的内联汇编代码,如果您尝试自己进行矢量化,编译器的生成可能比您想象的要好.
这-funsafe-math-optimizations是必要的,因为NEON指令不完全符合IEEE 754标准.正如GCC文件所述:
如果所选择的浮点硬件包含NEON扩展(例如
-mfpu=‘neon’),请注意,除非-funsafe-math-optimizations还指定了浮点运算,否则GCC的自动向量化传递不会生成浮点运算 .这是因为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;
}
Run Code Online (Sandbox Code Playgroud)
然而,使用您自己的类型的一个优点是,您可以改进代码编译器生成,对您的声明方式进行一个小的更改struct dcmplx:
typedef struct {
float re;
float im;
} __attribute__((aligned(8)) dcmplx;
Run Code Online (Sandbox Code Playgroud)
通过说它需要8字节(64位)对齐,这允许编译器跳过检查以查看它是否适当对齐,然后再回到较慢的标量实现上.
现在,假设你可以说你对GCC如何矢量化你的代码感到不满意,并认为你可以做得更好.这是否有理由使用内联汇编?不,接下来要尝试的是ARM NEON内在函数.使用内在函数就像普通的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;
}
Run Code Online (Sandbox Code Playgroud)
最后,如果这不够好并且您需要调整每一点性能,那么使用内联汇编仍然不是一个好主意.相反,你最后的选择应该是使用常规装配.由于您在汇编中重写了大部分函数,因此您可以在汇编中完全编写它.这意味着您不必担心告诉编译器您在内联汇编中所做的一切.您只需要符合ARM ABI,这可能很棘手,但比使用内联汇编使一切正确更容易.