ARM中乘法和存储的霓虹灯优化

Posted

技术标签:

【中文标题】ARM中乘法和存储的霓虹灯优化【英文标题】:Neon Optimization for multiplication and store in ARM 【发布时间】:2018-03-15 17:50:36 【问题描述】:

使用 ARM Cortex A15 板,我正在尝试通过使用 NEON 内在函数来优化完美运行的 C 代码。

编译器:ubuntu 12.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=hard

我想做下面的函数,它只是一个简单的加载->乘法->存储。

这里有一些参数: *input 是一个指向大小为 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");

【问题讨论】:

问题是什么?有错误要报告吗? 您需要从上一个问题中了解 cmets。 NEON 做得不好(正如您在上面所做的那样)不会比标量指令执行得更好。您需要使用大量寄存器来隐藏管道和内存延迟。如果这没有意义,那么您需要在编写 NEON 代码之前了解它。 你能解释一下这些问题在我的循环中发生在哪里吗?? 【参考方案1】:

嗯嗯,你的代码首先编译了吗?我不知道您可以将向量乘以浮点标量。可能编译器确实为您转换了。

无论如何,您必须了解大多数 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 个周期/迭代,其中只有 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);
    

现在我们正在处理 8 个独立向量,因此延迟被完全隐藏,潜在的缓存未命中惩罚以及平坦循环开销相当小。

【讨论】:

PS:如果您使用的是旧编译器,它可能无法识别float32x4x4_t。在这种情况下,您别无选择,只能将 vec1~vec8 声明为float32x4_t,并相应地映射它们。谈到 NEON,编写汇编代码比 IMO 更能得到回报。 float32x2_tvmul_n_f32(float32x2_t a, float32_t b);这是来自 arm 参考指南,所以我尝试编写的任何代码都来自那里,是的,现在我非常清楚地看到在这些情况下组装如何更好 veca = vld1q_f32_x4(pSrc++) gcc 不支持这个,所以我映射它veca.val[0] to [3],然后它编译但我想我的编译器又毁了它,周期急剧增加。 @Skynet NEON 内在函数在 GCC 版本 6.x 中得到了改进。如果可以的话,你应该试一试。是什么阻止您使用汇编? 我正在做这些作为我实习的一部分,gcc 4.7 是我得到的,我不能更改它,你能看看我在问题中的内联程序集吗?提供您的意见。谢谢。

以上是关于ARM中乘法和存储的霓虹灯优化的主要内容,如果未能解决你的问题,请参考以下文章

霓虹灯和手臂组装优化

为啥乘法、加法的霓虹内在函数比运算符慢?

霓虹灯:作为 IP 和 OP 的 64 位乘法和累加

如何在霓虹灯中进行交叉乘法?

关于 ARM NEON 周期的一些疑问

arm 霓虹灯比较操作产生负一