ARM 中乘法和存储的 Neon 优化
Neon Optimization for multiplication and store in ARM
使用 ARM Cortex A15 板,我正在尝试通过使用 NEON 内在函数来优化完美运行的 C 代码。
编译器:ubuntu12.04
上的 gcc 4.7
标志:-g -O3 -mcpu=cortex-a15 -mfpu=neon-vfpv4 -ftree-vectorize -DDRA7XX_ARM -DARM_PROC -DSL -funroll-loops -ftree-loop-ivcanon - mfloat-abi=硬
我想做以下功能,它只是一个简单的加载->乘法->存储。
这里有一些参数:
*输入是一个指向大小为 40680 的数组的指针,在完成循环后,指针应保留当前位置,并通过输入指针对下一个输入流执行相同的操作。
float32_t A=0.7;
float32_t *ptr_op=(float*)output[9216];
float32x2_t reg1;
for(i= 0;i< 4608;i+=4){
/*output[(2*i)] = A*(*input); // C version
input++;
output[(2*i)+1] = A*(*input);
input++;*/
reg1=vld1q_f32(input++); //Neon version
R_N=vmulq_n_f32(reg1,A);
vst1q_f32(ptr_op++,R_N);
}
我想知道我在这个循环中哪里出错了,因为它看起来很简单。
这是我对相同 .我走的方向对吗???
__asm__ __volatile__(
"\t mov r4, #0\n"
"\t vdup.32 d1,%3\n"
"Lloop2:\n"
"\t cmp r4, %2\n"
"\t bge Lend2\n"
"\t vld1.32 d0, [%0]!\n"
"\t vmul.f32 d0, d0, d1\n"
"\t vst1.32 d0, [%1]!\n"
"\t add r4, r4, #2\n"
"\t b Lloop2\n"
"Lend2:\n"
: "=r"(input), "=r"(ptr_op), "=r"(length), "=r"(A)
: "0"(input), "1"(ptr_op), "2"(length), "3"(A)
: "cc", "r4", "d1", "d0");
Hmmmmm,您的代码首先编译了吗?我不知道您可以将向量乘以浮点标量。可能编译器确实为您转换了 if。
无论如何,您必须了解大多数 NEON 指令都具有较长的延迟。除非你正确地隐藏它们,否则你的代码不会比标准 C 版本更快,甚至更慢。
vld1q..... // 1 cycle
// 4 cycles latency + potential cache miss penalty
vmulq..... // 2 cycles
// 6 cycles latency
vst1q..... // 1 cycle
// 2 cycles loop overhead
上面的示例粗略地显示了每次迭代所需的周期。
如您所见,最少 18 cycles/iteration,其中只有 4 个周期用于实际计算,而 14 个周期被无意义地浪费了。
叫做RAW dependency
(先写后读)
隐藏这些延迟的最简单且实际上唯一的方法是循环展开:深度循环。
每次迭代展开四个向量通常就足够了,如果您不介意代码长度,八个向量会更好。
void vecMul(float * pDst, float * pSrc, float coeff, int length)
{
const float32x4_t scal = vmovq_n_f32(coeff);
float32x4x4_t veca, vecb;
length -= 32;
if (length >= 0)
{
while (1)
{
do
{
length -= 32;
veca = vld1q_f32_x4(pSrc++);
vecb = vld1q_f32_x4(pSrc++);
veca.val[0] = vmulq_f32(veca.val[0], scal);
veca.val[1] = vmulq_f32(veca.val[1], scal);
veca.val[2] = vmulq_f32(veca.val[2], scal);
veca.val[3] = vmulq_f32(veca.val[3], scal);
vecb.val[0] = vmulq_f32(vecb.val[0], scal);
vecb.val[1] = vmulq_f32(vecb.val[1], scal);
vecb.val[2] = vmulq_f32(vecb.val[2], scal);
vecb.val[3] = vmulq_f32(vecb.val[3], scal);
vst1q_f32_x4(pDst++, veca);
vst1q_f32_x4(pDst++, vecb);
} while (length >= 0);
if (length <= -32) return;
pSrc += length;
pDst += length;
}
}
///////////////////////////////////////////////////////////////
if (length & 16)
{
veca = vld1q_f32_x4(pSrc++);
}
if (length & 8)
{
vecb.val[0] = vld1q_f32(pSrc++);
vecb.val[1] = vld1q_f32(pSrc++);
}
if (length & 4)
{
vecb.val[2] = vld1q_f32(pSrc++);
}
if (length & 2)
{
vld1q_lane_f32(pSrc++, vecb.val[3], 0);
vld1q_lane_f32(pSrc++, vecb.val[3], 1);
}
if (length & 1)
{
vld1q_lane_f32(pSrc, vecb.val[3], 2);
}
veca.val[0] = vmulq_f32(veca.val[0], scal);
veca.val[1] = vmulq_f32(veca.val[1], scal);
veca.val[2] = vmulq_f32(veca.val[2], scal);
veca.val[3] = vmulq_f32(veca.val[3], scal);
vecb.val[0] = vmulq_f32(vecb.val[0], scal);
vecb.val[1] = vmulq_f32(vecb.val[1], scal);
vecb.val[2] = vmulq_f32(vecb.val[2], scal);
vecb.val[3] = vmulq_f32(vecb.val[3], scal);
if (length & 16)
{
vst1q_f32_x4(pDst++, veca);
}
if (length & 8)
{
vst1q_f32(pDst++, vecb.val[0]);
vst1q_f32(pDst++, vecb.val[1]);
}
if (length & 4)
{
vst1q_f32(pDst++, vecb.val[2]);
}
if (length & 2)
{
vst1q_lane_f32(pDst++, vecb.val[3], 0);
vst1q_lane_f32(pDst++, vecb.val[3], 1);
}
if (length & 1)
{
vst1q_lane_f32(pDst, vecb.val[3], 2);
}
}
现在我们正在处理八个 独立 向量,因此延迟完全隐藏,并且潜在的缓存未命中惩罚以及平坦循环开销正在减少。
使用 ARM Cortex A15 板,我正在尝试通过使用 NEON 内在函数来优化完美运行的 C 代码。
编译器:ubuntu12.04
上的 gcc 4.7标志:-g -O3 -mcpu=cortex-a15 -mfpu=neon-vfpv4 -ftree-vectorize -DDRA7XX_ARM -DARM_PROC -DSL -funroll-loops -ftree-loop-ivcanon - mfloat-abi=硬
我想做以下功能,它只是一个简单的加载->乘法->存储。
这里有一些参数: *输入是一个指向大小为 40680 的数组的指针,在完成循环后,指针应保留当前位置,并通过输入指针对下一个输入流执行相同的操作。
float32_t A=0.7;
float32_t *ptr_op=(float*)output[9216];
float32x2_t reg1;
for(i= 0;i< 4608;i+=4){
/*output[(2*i)] = A*(*input); // C version
input++;
output[(2*i)+1] = A*(*input);
input++;*/
reg1=vld1q_f32(input++); //Neon version
R_N=vmulq_n_f32(reg1,A);
vst1q_f32(ptr_op++,R_N);
}
我想知道我在这个循环中哪里出错了,因为它看起来很简单。
这是我对相同 .我走的方向对吗???
__asm__ __volatile__(
"\t mov r4, #0\n"
"\t vdup.32 d1,%3\n"
"Lloop2:\n"
"\t cmp r4, %2\n"
"\t bge Lend2\n"
"\t vld1.32 d0, [%0]!\n"
"\t vmul.f32 d0, d0, d1\n"
"\t vst1.32 d0, [%1]!\n"
"\t add r4, r4, #2\n"
"\t b Lloop2\n"
"Lend2:\n"
: "=r"(input), "=r"(ptr_op), "=r"(length), "=r"(A)
: "0"(input), "1"(ptr_op), "2"(length), "3"(A)
: "cc", "r4", "d1", "d0");
Hmmmmm,您的代码首先编译了吗?我不知道您可以将向量乘以浮点标量。可能编译器确实为您转换了 if。
无论如何,您必须了解大多数 NEON 指令都具有较长的延迟。除非你正确地隐藏它们,否则你的代码不会比标准 C 版本更快,甚至更慢。
vld1q..... // 1 cycle
// 4 cycles latency + potential cache miss penalty
vmulq..... // 2 cycles
// 6 cycles latency
vst1q..... // 1 cycle
// 2 cycles loop overhead
上面的示例粗略地显示了每次迭代所需的周期。
如您所见,最少 18 cycles/iteration,其中只有 4 个周期用于实际计算,而 14 个周期被无意义地浪费了。
叫做RAW dependency
(先写后读)
隐藏这些延迟的最简单且实际上唯一的方法是循环展开:深度循环。
每次迭代展开四个向量通常就足够了,如果您不介意代码长度,八个向量会更好。
void vecMul(float * pDst, float * pSrc, float coeff, int length)
{
const float32x4_t scal = vmovq_n_f32(coeff);
float32x4x4_t veca, vecb;
length -= 32;
if (length >= 0)
{
while (1)
{
do
{
length -= 32;
veca = vld1q_f32_x4(pSrc++);
vecb = vld1q_f32_x4(pSrc++);
veca.val[0] = vmulq_f32(veca.val[0], scal);
veca.val[1] = vmulq_f32(veca.val[1], scal);
veca.val[2] = vmulq_f32(veca.val[2], scal);
veca.val[3] = vmulq_f32(veca.val[3], scal);
vecb.val[0] = vmulq_f32(vecb.val[0], scal);
vecb.val[1] = vmulq_f32(vecb.val[1], scal);
vecb.val[2] = vmulq_f32(vecb.val[2], scal);
vecb.val[3] = vmulq_f32(vecb.val[3], scal);
vst1q_f32_x4(pDst++, veca);
vst1q_f32_x4(pDst++, vecb);
} while (length >= 0);
if (length <= -32) return;
pSrc += length;
pDst += length;
}
}
///////////////////////////////////////////////////////////////
if (length & 16)
{
veca = vld1q_f32_x4(pSrc++);
}
if (length & 8)
{
vecb.val[0] = vld1q_f32(pSrc++);
vecb.val[1] = vld1q_f32(pSrc++);
}
if (length & 4)
{
vecb.val[2] = vld1q_f32(pSrc++);
}
if (length & 2)
{
vld1q_lane_f32(pSrc++, vecb.val[3], 0);
vld1q_lane_f32(pSrc++, vecb.val[3], 1);
}
if (length & 1)
{
vld1q_lane_f32(pSrc, vecb.val[3], 2);
}
veca.val[0] = vmulq_f32(veca.val[0], scal);
veca.val[1] = vmulq_f32(veca.val[1], scal);
veca.val[2] = vmulq_f32(veca.val[2], scal);
veca.val[3] = vmulq_f32(veca.val[3], scal);
vecb.val[0] = vmulq_f32(vecb.val[0], scal);
vecb.val[1] = vmulq_f32(vecb.val[1], scal);
vecb.val[2] = vmulq_f32(vecb.val[2], scal);
vecb.val[3] = vmulq_f32(vecb.val[3], scal);
if (length & 16)
{
vst1q_f32_x4(pDst++, veca);
}
if (length & 8)
{
vst1q_f32(pDst++, vecb.val[0]);
vst1q_f32(pDst++, vecb.val[1]);
}
if (length & 4)
{
vst1q_f32(pDst++, vecb.val[2]);
}
if (length & 2)
{
vst1q_lane_f32(pDst++, vecb.val[3], 0);
vst1q_lane_f32(pDst++, vecb.val[3], 1);
}
if (length & 1)
{
vst1q_lane_f32(pDst, vecb.val[3], 2);
}
}
现在我们正在处理八个 独立 向量,因此延迟完全隐藏,并且潜在的缓存未命中惩罚以及平坦循环开销正在减少。