ARM 中的 NEON 实现

Posted

技术标签:

【中文标题】ARM 中的 NEON 实现【英文标题】:NEON implementation in ARM 【发布时间】:2018-03-13 05:11:43 【问题描述】:

我是 NEON 的初学者,想优化以下代码,但是当它编译并产生与期望相同的输出时,我没有看到任何改进。 AFAIK NEON 有助于对连续数据块进行操作,所以我希望在执行时间和周期方面有所改进。我做错了什么?

我正在 Ubuntu 12.04 上使用 -03 级别优化的 gcc

普通的c实现

for(i= 0;i<9215;i++)
            
                Z[i] = (L[i]>0)?0:1;    
            

霓虹灯形式

for(i=0;i<9215;i+=4)
                                                                                   
             int32x4_t l_N = vld1q_s32(&L[i]);
            uint32x4_t mask_n=vcltq_s32(l_N,zero_N);
             int32x4_t z_n = vbslq_s32(mask_n,one_N,zero_N);
             vst1q_s32(&Z[i],z_n);


【问题讨论】:

你的例程没有引起异常几乎是一个奇迹。 9215 不是 4 的倍数。 而且计算部分效率也不高:vbsl 是一条相当昂贵的指令。你可以做一个饱和减法(vqsub),类型转换为无符号,然后移位 31(vshr_u32)。 【参考方案1】:

问题:

您正在使用一种非常低效的算法进行循环内的计算 您的例程遭受了繁重的流水线互锁,逐条指令
void isNonNatural(int32_t * pDst, int32_t *pSrc, int n)

    int32x4_t vec;
    const int32x4_t one = vdupq_n_s32(1);
    int32_t a;

    unsigned int i;

    if (n >= 4)
    
        n -= 4;
        while (1) 
            do 
                n -= 4;
                vec = vld1q_s32(pSrc++);
                vec = vqsubq_s32(vec, one);
                vec = (int32x4_t) vshrq_n_u32((uint32x4_t) vec, 31);
                vst1q_s32(pDst++, vec);
             while (n >= 0);

            if (n <= -4) return;

            // dealing with residuals

            pSrc += n;  // rewind pointers
            pDst += n;
         // iterate for one last time
    

    for (i = 0; i < n; ++i) 
        a = *pSrc++;
        if (a > 0) a = 0; else a = 1;
        *pDst++ = a;
    

上面的这个函数应该比你的实现要快一些。

执行饱和减法 1,使 0 变为 -1,而 0x80000000 仍为 0x80000000 元素被移动了 31 位,因此只保留了符号位。 我可以使用 0xffffffff 而不是 1,您可以省略类型转换并改用 vshrq_n_s32。不过也不会更快。 注意余量管理。

对 NEON 进行编程就像驾驶一辆大卡车。你不应该像开紧凑型车一样驾驶它。

虽然 NEON 可以一次计算多个数据,主要是在一个周期内,但它具有更高的指令延迟,通常为 3~4 个周期。换句话说,在上面的实现中,每条指令都必须等待上一条指令返回结果。

实际上,避免这种情况的唯一方法是展开,深入的。

void isNonNatural_unroll(int32_t * pDst, int32_t *pSrc, int n)

    int32x4_t vec1, vec2, vec3, vec4;
    const int32x4_t one = vdupq_n_s32(1);
    int32_t a;

    unsigned int i;

    if (n >= 16)
    
        n -= 16;
        while (1) 
            do 
                n -= 16;
                vec1 = vld1q_s32(pSrc++);
                vec2 = vld1q_s32(pSrc++);
                vec3 = vld1q_s32(pSrc++);
                vec4 = vld1q_s32(pSrc++);
                vec1 = vqsubq_s32(vec1, one);
                vec2 = vqsubq_s32(vec2, one);
                vec3 = vqsubq_s32(vec3, one);
                vec4 = vqsubq_s32(vec4, one);
                vec1 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec1, 31);
                vec2 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec2, 31);
                vec3 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec3, 31);
                vec4 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec4, 31);
                vst1q_s32(pDst++, vec1);
                vst1q_s32(pDst++, vec2);
                vst1q_s32(pDst++, vec3);
                vst1q_s32(pDst++, vec4);
             while (n >= 0);

            if (n <= -16) return;

            // dealing with residuals

            pSrc += n;  // rewind pointers
            pDst += n;
         // iterate for one last time
    

    if (n & 8)
    
        vec1 = vld1q_s32(pSrc++);
        vec2 = vld1q_s32(pSrc++);
        vec1 = vqsubq_s32(vec1, one);
        vec2 = vqsubq_s32(vec2, one);
        vec1 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec1, 31);
        vec2 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec2, 31);
        vst1q_s32(pDst++, vec1);
        vst1q_s32(pDst++, vec2);
    

    if (n & 4)
    
        vec1 = vld1q_s32(pSrc++);
        vec1 = vqsubq_s32(vec1, one);
        vec1 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec1, 31);
        vst1q_s32(pDst++, vec1);
    

    n &= 3;

    for (i = 0; i < n; ++i) 
        a = *pSrc++;
        if (a > 0) a = 0; else a = 1;
        *pDst++ = a;
    

现在这个应该比以前的快很多,因为几乎所有的延迟都被隐藏了(快四倍多),前提是可怜的编译器不会把它搞砸。

【讨论】:

非常感谢您的意见,我花了 2 个小时来理解您的实现,但最后我确实理解并在我的代码中实现了它并进行了更改,但它大大增加了周期,我很抱歉给索引为 9215,实际上是 9216 加 1 用于承认编译器如何搞砸完美的内在函数 :) 是的,@BitBank 我认为你是对的,但现在我无法更改我的编译器。 @Skynet 我愿意为aarch32aarch64 提供完全优化的汇编版本(这是我的专长)。你会发布基准测试结果吗? @Jake'Alquimista'LEE 非常感谢你的帮助,但我目前受到一些限制,我无法使用汇编语言,但我尝试了内联汇编,它适用于非常简单的事情喜欢浮动到固定的转换,所以是的,我肯定会给出基准。

以上是关于ARM 中的 NEON 实现的主要内容,如果未能解决你的问题,请参考以下文章

ARM NEON指令集总结

使用NEON优化ARM的卷积运算

ARM NEON 中的指令调度

带有 NEON 的 ARM 汇编中的高级数学函数

在 x86(使用 SSE2)和 ARM(使用 vfpv4 NEON)上尾数为 11 位的 atan2 近似值

在简单的加法任务中使用 ARM NEON 速度较慢