5

学习 ARM NEON 内在函数时,我正在计时一个我编写的函数以将数组中的元素加倍。使用内在函数的版本比函数的普通 C 版本需要更多时间。

没有霓虹灯:

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

使用霓虹灯:

 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;
}

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

更新:更多信息以回应 Igor 的回复。
1.代码贴在这里:
plain.c
plain.s
neon.c
neon.s
从两个汇编列表中的第(标签)L7部分,我看到霓虹版本有更多的汇编指令。(因此更多时间采取?)
2.我在 arm-gcc 上使用 -mfpu=neon 编译,没有其他标志或优化。对于普通版本,根本没有编译器标志。
3.那是一个错字,SIZE 是指尺寸;两者都是一样的。
4,5.尝试了一个包含 4000 个元素的数组。我在函数调用前后使用 gettimeofday() 进行计时。NEON=230us,ordinary=155us。
6.是的,我在每种情况下都打印了元素。
7.这样做了,没有任何改善。

4

3 回答 3

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]
  • 您还可以使用后增量版本(我也不确定内在函数是否会生成),例如vld1.32 {q0}, [r1 :128]!

4000 个元素的 95us 听起来很慢,在 1GHz 处理器上,每 128 位块大约 95 个周期。假设您从缓存中工作,您应该能够做得更好。如果您受到外部存储器速度的限制,这个数字是您所期望的。

于 2011-06-13T00:36:31.387 回答
3

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

  1. 在查看程序集之前,您无法确定发生了什么。使用 -S,卢克!
  2. 您没有指定编译器设置。你在使用优化吗?循环展开?
  3. 第一个功能使用size,第二个使用SIZE,这是故意的吗?他们是一样的吗?
  4. 您尝试的数组的大小是多少?我不希望 NEON 对几个元素有帮助。
  5. 速度差异是多少百分之几?几个数量级?
  6. 你检查结果是否相同?你确定代码是等价的吗?
  7. 您对中间结果使用相同的变量。尝试将加法的结果存储在另一个变量中,这可能会有所帮助(尽管我希望编译器会很聪明并分配一个不同的寄存器)。此外,您可以尝试使用 shift ( vshl_n_u32) 代替加法。

编辑:感谢您的回答。我环顾四周,发现了这个讨论,上面写着(强调我的):

将数据从 NEON 移动到 ARM 寄存器是 Cortex-A8 的昂贵,因此 Cortex-A8 中的 NEON 最适合用于具有很少 ARM 流水线交互的大型工作块。

在您的情况下,没有 NEON 到 ARM 的转换,只有加载和存储。尽管如此,并行操作的节省似乎被非 NEON 部件吃掉了。我希望在 NEON 中执行许多操作的代码会产生更好的结果,例如颜色转换。

于 2011-04-19T13:51:34.520 回答
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条指令流水线,1条执行,1条保存结果)。只要您保持这两个管道繁忙并交错您的指令,它就会非常快速地工作。更好的是,如果你有 ARM 指令,只要你留在寄存器中,它就永远不必等待 NEON 完成,它们会同时执行(缓存中最多 8 条指令)!所以你可以在ARM指令中加入一些基本的循环逻辑,它们会同时执行。

您的原始代码也只使用了 4 个寄存器值中的一个(q 寄存器有 4 个 32 位值)。其中 3 人无缘无故地进行了翻倍手术,所以你的速度是原本可以达到的速度的 4 倍。

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

最后一部分......我说的可能是真的,但并非总是如此。这取决于您拥有的当前 Neon 版本。未来的时间可能会改变,或者可能并不总是这样。它对我有用,ymmv。

于 2011-11-22T19:22:54.923 回答