NEON怎么会像C一样慢?
我一直在尝试构建一个快速的直方图函数,通过为它们分配一个值(即它们最接近的范围阈值)将输入值分成范围。这是应用于图像的东西,因此它必须快速(假设图像阵列为640x480,因此300,000个元素)。直方图范围数是倍数(0,25,50,75,100)。输入将是浮点数,最终输出显然是整数
我通过打开一个新的空项目(没有app委托)并使用main.m文件在xCode上测试了以下版本。我删除了所有链接库,但Accelerate除外。
这是C实现:如果那时旧版本很多,但这里是最终的优化逻辑。花了11秒和300毫秒。
int main(int argc, char *argv[])
{
NSLog(@"starting");
int sizeOfArray=300000;
float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
int* outputArray=(int*) malloc(sizeof(int)*sizeOfArray);
for (int i=0; i<sizeOfArray; ++i)
{
inputArray[i]=88.5;
}
//Assume range is [0,25,50,75,100]
int lcd=25;
for (int j=0; j<1000; ++j)// just to get some good time interval
{
for (int i=0; i<sizeOfArray; ++i)
{
//a 60.5 would give a 50. An 88.5 would give 100
outputArray[i]=roundf(inputArray[i]/lcd)*lcd;
}
}
NSLog(@"done");
}
这是vDSP实现。即使有一些繁琐的浮动到整数来回,它只花了6秒!几乎提高了50%!
//vDSP implementation
int main(int argc, char *argv[])
{
NSLog(@"starting");
int sizeOfArray=300000;
float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
float* outputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);//vDSP requires matching of input output
int* outputArray=(int*) malloc(sizeof(int)*sizeOfArray); //rounded value to the nearest integere
float* finalOutputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);
int* finalOutputArray=(int*) malloc(sizeof(int)*sizeOfArray); //to compare apples to apples scenarios output
for (int i=0; i<sizeOfArray; ++i)
{
inputArray[i]=37.0; //this will produce an final number of 25. On the other hand 37.5 would produce 50.
}
for (int j=0; j<1000; ++j)// just to get some good time interval
{
//Assume range is [0,25,50,75,100]
float lcd=25.0f;
//divide by lcd
vDSP_vsdiv(inputArray, 1, &lcd, outputArrayF, 1,sizeOfArray);
//Round to nearest integer
vDSP_vfixr32(outputArrayF, 1,outputArray, 1, sizeOfArray);
// MUST convert int to float (cannot just cast) then multiply by scalar - This step has the effect of rounding the number to the nearest lcd.
vDSP_vflt32(outputArray, 1, outputArrayF, 1, sizeOfArray);
vDSP_vsmul(outputArrayF, 1, &lcd, finalOutputArrayF, 1, sizeOfArray);
vDSP_vfix32(finalOutputArrayF, 1, finalOutputArray, 1, sizeOfArray);
}
NSLog(@"done");
}
这是霓虹灯实施。这是我的第一次好玩!它比vDSP慢,花了9秒300毫秒,对我来说没有意义。 vDSP比NEON更好地优化,或者我做错了。
//NEON implementation
int main(int argc, char *argv[])
{
NSLog(@"starting");
int sizeOfArray=300000;
float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
float* finalOutputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);
for (int i=0; i<sizeOfArray; ++i)
{
inputArray[i]=37.0; //this will produce an final number of 25. On the other hand 37.5 would produce 50.
}
for (int j=0; j<1000; ++j)// just to get some good time interval
{
float32x4_t c0,c1,c2,c3;
float32x4_t e0,e1,e2,e3;
float32x4_t f0,f1,f2,f3;
//ranges of histogram buckets
float32x4_t buckets0=vdupq_n_f32(0);
float32x4_t buckets1=vdupq_n_f32(25);
float32x4_t buckets2=vdupq_n_f32(50);
float32x4_t buckets3=vdupq_n_f32(75);
float32x4_t buckets4=vdupq_n_f32(100);
//midpoints of ranges
float32x4_t thresholds1=vdupq_n_f32(12.5);
float32x4_t thresholds2=vdupq_n_f32(37.5);
float32x4_t thresholds3=vdupq_n_f32(62.5);
float32x4_t thresholds4=vdupq_n_f32(87.5);
for (int i=0; i<sizeOfArray;i+=16)
{
c0= vld1q_f32(&inputArray[i]);//load
c1= vld1q_f32(&inputArray[i+4]);//load
c2= vld1q_f32(&inputArray[i+8]);//load
c3= vld1q_f32(&inputArray[i+12]);//load
f0=buckets0;
f1=buckets0;
f2=buckets0;
f3=buckets0;
//register0
e0=vcgtq_f32(c0,thresholds1);
f0=vbslq_f32(e0, buckets1, f0);
e0=vcgtq_f32(c0,thresholds2);
f0=vbslq_f32(e0, buckets2, f0);
e0=vcgtq_f32(c0,thresholds3);
f0=vbslq_f32(e0, buckets3, f0);
e0=vcgtq_f32(c0,thresholds4);
f0=vbslq_f32(e0, buckets4, f0);
//register1
e1=vcgtq_f32(c1,thresholds1);
f1=vbslq_f32(e1, buckets1, f1);
e1=vcgtq_f32(c1,thresholds2);
f1=vbslq_f32(e1, buckets2, f1);
e1=vcgtq_f32(c1,thresholds3);
f1=vbslq_f32(e1, buckets3, f1);
e1=vcgtq_f32(c1,thresholds4);
f1=vbslq_f32(e1, buckets4, f1);
//register2
e2=vcgtq_f32(c2,thresholds1);
f2=vbslq_f32(e2, buckets1, f2);
e2=vcgtq_f32(c2,thresholds2);
f2=vbslq_f32(e2, buckets2, f2);
e2=vcgtq_f32(c2,thresholds3);
f2=vbslq_f32(e2, buckets3, f2);
e2=vcgtq_f32(c2,thresholds4);
f2=vbslq_f32(e2, buckets4, f2);
//register3
e3=vcgtq_f32(c3,thresholds1);
f3=vbslq_f32(e3, buckets1, f3);
e3=vcgtq_f32(c3,thresholds2);
f3=vbslq_f32(e3, buckets2, f3);
e3=vcgtq_f32(c3,thresholds3);
f3=vbslq_f32(e3, buckets3, f3);
e3=vcgtq_f32(c3,thresholds4);
f3=vbslq_f32(e3, buckets4, f3);
vst1q_f32(&finalOutputArrayF[i], f0);
vst1q_f32(&finalOutputArrayF[i+4], f1);
vst1q_f32(&finalOutputArrayF[i+8], f2);
vst1q_f32(&finalOutputArrayF[i+12], f3);
}
}
NSLog(@"done");
}
PS:这是我在这个规模上的第一个基准测试,所以我试着保持简单(大循环,设置代码不变,使用NSlog打印开始/结束时间,只加速框架链接)。如果这些假设中的任何一个对结果产生重大影响,请批评。
由于
答案 0 :(得分:6)
首先,这不是“NEON”本身。这是内在的。使用clang或gcc下的内在函数几乎不可能获得良好的NEON性能。如果您认为需要内在函数,则应手写汇编程序。
vDSP没有比NEON“更好地优化”。 iOS上的vDSP使用NEON处理器。 vDSP使用NEON比使用NEON要好得多。
我还没有挖掘你的内在函数代码,但最可能(实际上几乎可以肯定)麻烦的原因是你正在创建等待状态。用汇编语言编写(和内在函数只是用焊接手套编写的汇编程序),就像用C语言编写一样。你不要循环使用它。你不比较相同。你需要一种新的思维方式。在汇编中,你可以一次做多件事(因为你有不同的逻辑单元),但是你必须以这样的方式安排事情,使所有这些东西可以并行运行。良好的装配使所有这些管道都充满了。如果您可以阅读您的代码并且它非常有意义,那么它可能是废话汇编代码。如果你从不重复自己,那可能就是废话汇编代码。在您被允许阅读之前,您需要仔细考虑进入哪个寄存器以及有多少个周期。
如果它像音译C一样简单,那么编译器会为你做到这一点。你说“我要在NEON中写这个”的那一刻你会说“我认为我可以编写比编译器更好的NEON”,因为编译器也使用它。也就是说,通常可以编写比编译器更好的NEON(特别是gcc和clang)。
如果你准备好潜入那个世界(这是一个非常酷的世界),你就会有一些阅读。这是我推荐的一些地方:
所有这一切 ...始终始终重新考虑您的算法。答案通常不是如何快速计算循环,而是如何不经常调用循环。
答案 1 :(得分:4)
ARM NEON有32个寄存器,64位宽(双视图为16个寄存器,128位宽)。你的霓虹灯实现已经使用了至少18个128位宽,因此编译器会生成代码来从堆栈中来回移动它们,这并不好 - 太多的额外内存访问。
如果您打算使用汇编程序,我发现最好使用工具转储目标文件中的指令。一个在Linux中被称为objdump
,我相信它在Apple世界中被称为otool
。通过这种方式,您可以实际查看生成的机器代码的样子,以及编译器对您的函数执行的操作。
以下是你的霓虹灯实现从gcc(-O3)4.7.1转储的一部分。您可以注意到通过vldmia sp, {d8-d9}
加载四元组寄存器。
1a6: ff24 cee8 vcgt.f32 q6, q10, q12
1aa: ff64 4ec8 vcgt.f32 q10, q10, q4
1ae: ff2e a1dc vbit q5, q15, q6
1b2: ff22 ceea vcgt.f32 q6, q9, q13
1b6: ff5c 41da vbsl q10, q14, q5
1ba: ff20 aeea vcgt.f32 q5, q8, q13
1be: f942 4a8d vst1.32 {d20-d21}, [r2]!
1c2: ec9d 8b04 vldmia sp, {d8-d9}
1c6: ff62 4ee8 vcgt.f32 q10, q9, q12
1ca: f942 6a8f vst1.32 {d22-d23}, [r2]
当然这完全取决于编译器,更好的编译器可以通过更清楚地使用可用寄存器来避免这种情况。
因此,如果不使用汇编(内联,独立),或者应该不断检查编译器输出,直到从中获得所需内容,最终你将受编译器的支配。
答案 2 :(得分:2)
作为Rob的答案的补充,编写NEON本身就是一门艺术(顺便说一下,感谢插入我的漫游编码器帖子),以及auselen的回答(你确实在任何给定的情况下都有太多的寄存器时间,导致溢出),我应该补充一点,你的内在算法比其他两个更通用:它允许任意范围,而不仅仅是倍数,所以你试图比较不具有可比性的东西。总是将橙子与橙子进行比较;但是,如果您只需要自定义算法的特定功能,那么比较自定义算法比现成的通用算法更公平。所以这是NEON算法可以像C一样慢的另一种方式:如果它们不是相同的算法。
至于你的直方图需求,暂时使用你用vDSP构建的内容,如果性能不满足你的应用程序,只使用 ,然后再研究优化另一种方式;除了使用NEON指令之外,这样做的途径包括避免这么多的内存移动(可能是vDSP实现中的瓶颈),并在浏览像素时递增每个桶的计数器,而不是让这个中间输出由强制转换值。高效的DSP代码不仅仅涉及计算本身,还涉及如何最有效地使用内存带宽等等。在移动设备上更是如此:内存I / O,甚至是高速缓存,比处理器内核操作更耗电,因此内存I / O总线往往以较低的处理器时钟速度运行,所以你没有那么多的内存带宽,和你应该明智地使用你拥有的内存带宽,因为任何使用它都需要电源。