了解ARM NEON的内在特性时,我编写了一个函数,用于将数组中的元素翻倍。使用内部函数的版本比使用普通C版本的函数花费更多的时间。
Without NEON:
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;
}
想知道数组和vector之间的load/store操作是否消耗了更多的时间,从而抵消了并行加法的好处。
更新:更多信息回应Igor的回复。
1.代码张贴在这里:
plain.c
plain.s
neon.c
neon.s
从两个汇编清单中的L7部分(标签)中,我看到霓虹灯版本有更多的汇编指令。(因此需要更多的时间?)
2.我在arm-gcc上使用-mfpu=neon进行编译,没有其他标志或优化。对于普通版本,根本没有编译器标志。
3.这是一个打字错误,SIZE应该是SIZE,两者是一样的。
4、5。在包含4000个元素的数组上尝试。我在函数调用前后分别使用gettimeofday()计时。neon =230us,ordinary=155us。
6.是的,我打印了每个case中的元素。
7.这样做,没有任何改善。
这样的操作可能会运行得更快一些。
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内部参数预加载数据来加快速度。
- 也正如其他人指出的那样,该操作相当微不足道,您将看到更复杂的操作获得更多收益。
如果你要用内联汇编编写这个,你还可以:
- 使用对齐的load/stores(我不认为intrinsic可以生成)并确保您的指针始终是128位对齐的,例如vld1.32 {q0}, [r1:128]
- 你也可以使用后增量版本(我也不确定intrinsic会生成),例如vld1.32 {q0}, [r1:128]!
95us听起来相当慢,在1GHz处理器上,每个128位块约95个周期。假设您正在缓存中工作,您应该能够做得更好。如果你受到外部存储器速度的限制,这个数字就是你所期望的。
这个问题很模糊,你也没有提供太多信息,但我会试着给你一些提示。
- 你不知道到底发生了什么,直到你看到组装。用-S,卢克! 你没有指定编译器设置。你在使用优化吗?循环展开?
- 第一个函数使用
size
,第二个使用SIZE
,这是故意的吗?它们是一样的吗? - 您尝试的数组的大小是多少?我不期望NEON对几个元素有任何帮助。
- 什么是速度差?几个百分点?几个数量级?
- 你检查结果是否一致?你确定代码是相等的吗?
- 您使用相同的变量作为中间结果。尝试将添加的结果存储在另一个变量中,这可能会有所帮助(尽管我希望编译器会很聪明并分配不同的寄存器)。此外,您可以尝试使用shift (
vshl_n_u32
)代替添加。
编辑:谢谢你的回答。我看了一下周围,发现了这个讨论,它说(强调我的):
将数据从NEON寄存器移动到ARM寄存器是Cortex-A8很贵,所以NEON在吗Cortex-A8最适合用于large工作块与小ARM管道相互作用。
在你的情况下,没有NEON到ARM的转换,只有加载和存储。然而,并行操作的节省似乎被非neon部分消耗掉了。我希望在NEON中做很多事情的代码中得到更好的结果,例如颜色转换。
每条指令处理更大的数量,并交错加载/存储和交错使用。该函数当前为双精度(左移)56个单位。
void shiftleft56(const unsigned int* input, unsigned int* output)
{
__asm__ (
"vldm %0!, {q2-q8}nt"
"vldm %0!, {q9-q15}nt"
"vshl.u32 q0, q2, #1nt"
"vshl.u32 q1, q3, #1nt"
"vshl.u32 q2, q4, #1nt"
"vshl.u32 q3, q5, #1nt"
"vshl.u32 q4, q6, #1nt"
"vshl.u32 q5, q7, #1nt"
"vshl.u32 q6, q8, #1nt"
"vshl.u32 q7, q9, #1nt"
"vstm %1!, {q0-q6}nt"
// "vldm %0!, {q0-q6}nt" if you want to overlap...
"vshl.u32 q8, q10, #1nt"
"vshl.u32 q9, q11, #1nt"
"vshl.u32 q10, q12, #1nt"
"vshl.u32 q11, q13, #1nt"
"vshl.u32 q12, q14, #1nt"
"vshl.u32 q13, q15, #1nt"
// lost cycle here unless you overlap
"vstm %1!, {q7-q13}nt"
: "=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倍。
在这段代码中更好的是,通过在vstm %1!
之后添加vldm %0!, {q2-q8}
来处理嵌入的循环。等等......您还可以看到,在发送结果之前,我又等待了1条指令,因此管道永远不会等待其他东西。最后,注意!
,它表示后增量。所以它读/写这个值,然后自动从寄存器中增加指针。我建议你不要在ARM代码中使用寄存器,这样它就不会挂起自己的管道…让你的寄存器分开,在ARM端有一个冗余的count
变量。
最后一部分…我说的可能是对的,但不总是对的。这取决于当前的Neon版本。时间可能会在未来改变,或者可能不是一直这样。