使用内在函数的霓虹灯优化

时间:2011-04-19 13:21:38

标签: arm neon cortex-a8

了解ARM NEON内在函数,我正在计算一个函数,我编写的函数将数组中的元素加倍。使用内在函数的版本比函数的普通C版本花费更多时间。

没有NEON:

    void  double_elements(unsigned int *ptr, unsigned int size)
 {
        unsigned int loop;
        for( loop= 0; loop<size; loop++)
                ptr[loop]<<=1;
        return;
 }

使用NEON:

 void  double_elements(unsigned int *ptr, unsigned int size)
{    
        unsigned int i;
        uint32x4_t Q0,vector128Output;
        for( i=0;i<(SIZE/4);i++)
        {
                Q0=vld1q_u32(ptr);               
                Q0=vaddq_u32(Q0,Q0);
                vst1q_u32(ptr,Q0);
                ptr+=4;

        }
        return;
}

想知道数组和向量之间的加载/存储操作是否消耗了更多的时间来抵消并行添加的好处。

更新:更多信息以回应伊戈尔的回复 1.代码张贴在这里:
  plain.c
  plain.s
  neon.c
  neon.s
从两个装配清单中的部分(标签)L7,我看到霓虹灯版本有更多的装配说明。(因此需要更多时间?)
2.我在arm-gcc上使用-mfpu = neon编译,没有其他标志或优化。对于普通版本,根本没有编译器标志。
那是一个错字,SIZE本来就是大小;两者都是一样的。
4,5。由4000个元素组成。我在函数调用之前和之后使用gettimeofday()定时.NEON = 230us,普通= 155us 是的,我在每种情况下都打印了元素 除此之外,没有任何进步。

3 个答案:

答案 0 :(得分:4)

这样的事情可能会更快一些。

void  double_elements(unsigned int *ptr, unsigned int size)
{    
    unsigned int i;
    uint32x4_t Q0,Q1,Q2,Q3;

    for( i=0;i<(SIZE/16);i++)
    {
            Q0=vld1q_u32(ptr);               
            Q1=vld1q_u32(ptr+4);               
            Q0=vaddq_u32(Q0,Q0);
            Q2=vld1q_u32(ptr+8);               
            Q1=vaddq_u32(Q1,Q1);
            Q3=vld1q_u32(ptr+12);               
            Q2=vaddq_u32(Q2,Q2);
            vst1q_u32(ptr,Q0);
            Q3=vaddq_u32(Q3,Q3);
            vst1q_u32(ptr+4,Q1);
            vst1q_u32(ptr+8,Q2);
            vst1q_u32(ptr+12,Q3);
            ptr+=16;

    }
    return;
}

原始代码存在一些问题(优化程序可能会修复其中一些问题但其他问题可能没有,您需要在生成的代码中进行验证):

  • 添加的结果仅在NEON管道的N3阶段可用,因此以下商店将停止。
  • 假设编译器没有展开循环,可能会有一些与循环/分支相关的开销。
  • 它没有利用双重问题加载/存储与另一个NEON指令的能力。
  • 如果源数据不在缓存中,则负载将停止。您可以使用 __ builtin_prefetch 内在函数预加载数据以加快速度。
  • 另外,正如其他人指出的那样,操作相当简单,你会看到更多复杂操作的收益。

如果您使用内联汇编编写此内容,您还可以:

  • 使用对齐的加载/存储(我不认为内在函数可以生成)并确保指针总是128位对齐,例如 vld1.32 {q0},[r1:128]
  • 您也可以使用postincrement版本(我也不确定内在函数会生成),例如: vld1.32 {q0},[r1:128]!
对于4000个元素来说,95us听起来相当慢,在1GHz处理器上,每128bit块大约95个周期。假设你在缓存中工作,你应该能够做得更好。如果你受到外部存储器速度的限制,这个数字就是你所期望的。

答案 1 :(得分:3)

这个问题相当模糊,你没有提供太多信息,但我会尽力给你一些指示。

  1. 在你看到装配之前,你不会确切知道发生了什么。使用-S,Luke!
  2. 您没有指定编译器设置。你在使用优化吗?循环展开?
  3. 第一个函数使用size,第二个函数使用SIZE,这是故意的吗?它们是一样的吗?
  4. 您尝试过的阵列大小是多少?我不认为NEON可以为一些元素提供帮助。
  5. 什么是速度差异?几个百分点?几个数量级的订单?
  6. 您是否检查过结果是否相同?你确定代码是等价的吗?
  7. 您使用相同的变量作为中间结果。尝试将添加的结果存储在另一个变量中,这可能有所帮助(虽然我希望编译器是智能的并分配不同的寄存器)。此外,您可以尝试使用shift(vshl_n_u32)而不是添加。

  8. 编辑:谢谢你的回答。我看了一下,找到了this discussion,它说(强调我的):

      

    将数据从NEON移动到ARM寄存器   是Cortex-A8很贵,所以NEON在   Cortex-A8最适用于大   小ARM的工作块   管道互动。

    在你的情况下,没有NEON到ARM的转换,只有加载和存储。但是,非NEON部件似乎消耗了并行操作的节省。我希望在NEON中执行很多操作的代码可以获得更好的结果,例如颜色转换。

答案 2 :(得分:3)

按指令处理更大的数量,并交错加载/存储和交错使用。此功能目前翻倍(向左移动)56 uint。

void shiftleft56(const unsigned int* input, unsigned int* output)
{
  __asm__ (
  "vldm %0!, {q2-q8}\n\t"
  "vldm %0!, {q9-q15}\n\t"
  "vshl.u32 q0, q2, #1\n\t"
  "vshl.u32 q1, q3, #1\n\t"
  "vshl.u32 q2, q4, #1\n\t"
  "vshl.u32 q3, q5, #1\n\t"
  "vshl.u32 q4, q6, #1\n\t"
  "vshl.u32 q5, q7, #1\n\t"
  "vshl.u32 q6, q8, #1\n\t"
  "vshl.u32 q7, q9, #1\n\t"
  "vstm %1!, {q0-q6}\n\t"
  // "vldm %0!, {q0-q6}\n\t" if you want to overlap...
  "vshl.u32 q8, q10, #1\n\t"
  "vshl.u32 q9, q11, #1\n\t"
  "vshl.u32 q10, q12, #1\n\t"
  "vshl.u32 q11, q13, #1\n\t"
  "vshl.u32 q12, q14, #1\n\t"
  "vshl.u32 q13, q15, #1\n\t"
  // lost cycle here unless you overlap
  "vstm %1!, {q7-q13}\n\t"
  : "=r"(input), "=r"(output) : "0"(input), "1"(output)
  : "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7",
    "q8", "q9", "q10", "q11", "q12", "q13", "q14", "q15", "memory" );
}

重要的是要记住Neon优化......它有两个管道,一个用于加载/存储(带有2个指令队列 - 一个待处理,一个正在运行 - 通常每个需要3-9个周期),另一个用于算术运算(使用2个指令管道,一个执行,一个保存其结果)。只要您保持这两个管道繁忙并交错指示,它就会非常快速地工作。更好的是,如果你有ARM指令,只要你留在寄存器中,它就永远不必等待NEON完成,它们将同时执行(高速缓存中最多8条指令)!因此,您可以在ARM指令中设置一些基本的循环逻辑,它们将同时执行。

您的原始代码也仅使用4个寄存器值(q寄存器有4个32位值)。他们中的3人在没有明显原因的情况下进行了双倍的操作,所以你的速度是你原来的4倍。

此代码中更好的是针对此循环,通过在vldm %0!, {q2-q8}之后添加vstm %1!来处理它们,等等。你还看到我在发送结果之前再等一个指令,所以管道永远不会等待别的东西。最后,请注意!,这意味着后增量。因此它读取/写入值,然后自动从寄存器递增指针。我建议你不要在ARM代码中使用该寄存器,因此它不会挂起自己的管道......保持寄存器分离,在ARM端有一个冗余的count变量。

最后一部分......我所说的可能是真的,但并非总是如此。这取决于您目前的霓虹灯修订版。时间可能在未来发生变化,或者可能并非总是如此。它适用于我,ymmv。