问题描述
|
了解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;
}
想知道数组和向量之间的加载/存储操作是否正在消耗更多时间,这抵消了并行加法的好处。
更新:更多信息响应Igor的回复。
1.代码发布在这里:
普通的
平原
霓虹灯
霓虹灯
从两个汇编清单的L7部分中,我看到霓虹灯版本的汇编指令数量更多(因此需要更多时间吗?)
2.我在arm-gcc上使用-mfpu = neon进行编译,没有其他标志或优化。对于普通版本,根本没有编译器标志。
3.那是一个错字,SIZE就是大小;两者都是一样的。
4,5。尝试了4000个元素的数组。我在函数调用前后使用gettimeofday()进行计时。NEON= 230us,ordinary = 155us。
6.是的,我在每种情况下都打印了元素。
7.Did,没有任何改善。解决方法
问题相当模糊,您没有提供太多信息,但我会尽力为您提供一些指导。
在查看程序集之前,您不确定会发生什么情况。使用-S,卢克!
您没有指定编译器设置。您在使用优化吗?循环展开?
第一个函数使用
size
,第二个函数使用SIZE
,这是故意的吗?他们是一样的吗?
您尝试过的阵列大小是多少?我不希望NEON在两个方面都提供帮助。
速度差是多少?几个百分点?几个数量级?
您是否检查过结果是否相同?您确定代码是等效的吗?
您正在为中间结果使用相同的变量。尝试将加法结果存储在另一个变量中,这可能会有所帮助(尽管我希望编译器会很聪明,并分配一个不同的寄存器)。另外,您可以尝试使用shift(vshl_n_u32
)代替加法。
编辑:感谢您的答案。我四处张望,发现了这个讨论,说(强调我的):
将数据从NEON移至ARM寄存器
是Cortex-A8很贵,所以NEON在
Cortex-A8最适合用于大型
小ARM工作块
管道交互。
在您的情况下,没有NEON到ARM的转换,而只能加载和存储。尽管如此,非NEON部件似乎吞噬了并行操作的节省。我希望在NEON中可以完成许多工作的代码中获得更好的结果,例如颜色转换。,这样的事情可能会运行得更快。
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]!
在1GHz处理器上,每128位块约有95个周期,因此4000个元素的95us听起来很慢。假设您正在使用缓存,那么您应该能够做得更好。这个数字是关于如果您受外部存储器速度的约束所期望的。,每条指令的处理量更大,并交错加载/存储和交错使用。该功能当前加倍(向左移动)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倍。
在此代码中最好的办法是对该循环进行处理,方法是在vstm %1!
之后添加vldm %0!,{q2-q8}
,从而对它们进行嵌入处理,依此类推。您还会看到我在发送结果之前还要再等待1条指令,因此管道永远不会等待其他指令。最后,注意“ 9”,表示后递增。因此它读取/写入该值,然后自动从寄存器中递增指针。我建议您不要在ARM代码中使用该寄存器,因此它不会挂起自己的管道...使您的寄存器分开,在ARM端有一个冗余的count
变量。
最后一部分...我说的可能是正确的,但并非总是如此。这取决于您当前的Neon版本。时间可能会在将来发生变化,或者可能并非一直如此。 ymmv,它对我有用。