背景(如果您愿意,请跳过此内容)
首先让我说我不是专家程序员。我是一名年轻的初级计算机视觉(CV)工程师,我在C ++编程方面经验丰富,主要是因为大量使用了优秀的OpenCV2 C ++ API。我所学到的只是通过执行项目的需要,解决问题和满足最后期限的需要,因为这是行业的现实。
最近,我们开始为嵌入式系统(ARM板)开发CV软件,我们使用普通的C ++优化代码。然而,与传统计算机相比,由于资源有限,在这种架构中构建实时CV系统是一个巨大的挑战。
那是我发现NEON的时候。我已经阅读了很多关于此的文章,但这是一个相当新的主题,因此没有太多关于它的信息,我读的越多,我就越困惑。
问题
我希望使用一次计算4或8个数组元素的NEON功能来优化C ++代码(主要是一些 for loops )。是否有某种库或一组函数可以在C ++环境中使用?我混淆的主要原因是我看到的几乎所有代码snipets都在汇编中,我完全没有背景,并且在这一点上可能无法学习。 我在Linux Gentoo中使用Eclipse IDE来编写C ++代码。
更新
阅读完答案后,我用软件做了一些测试。我使用以下标志编译了我的项目:
-O3 -mcpu=cortex-a9 -ftree-vectorize -mfloat-abi=hard -mfpu=neon
请记住,这个项目包括openframeworks,OpenCV和OpenNI等大量库,所有内容都是用这些标志编译的。 为了编译ARM板,我们使用Linaro工具链交叉编译器,GCC的版本是4.8.3。 您是否希望这可以改善项目的性能?因为我们没有经历任何变化,考虑到我在这里读到的所有答案,这是相当奇怪的。
另一个问题:所有 for cycles 都有明显的迭代次数,但其中很多迭代自定义数据类型(结构或类)。 GCC可以优化这些周期,即使它们遍历自定义数据类型吗?
答案 0 :(得分:13)
编辑:
从您的更新中,您可能会误解NEON处理器的功能。它是SIMD(单指令,多数据)矢量处理器。这意味着它非常擅长同时对多个数据执行指令(比如说"乘以4")。它也喜欢做"将所有这些数字加在一起"或者"添加这两个数字列表中的每个元素以创建第三个数字列表。"所以,如果你的问题看起来像NEON处理器那将是巨大的帮助。
为了获得这些好处,您必须将数据放在非常特定的格式中,以便矢量处理器可以同时加载多个数据,并行处理,然后同时将其写回。你需要组织一些事情,使数学避免大多数条件(因为过早查看结果意味着往返NEON)。矢量编程是一种不同的思考方案的方式。这完全是关于管道管理的。
现在,对于许多非常常见的问题,编译器会自动完成所有这些工作。但它仍然在处理数字和特定格式的数字。例如,您几乎总是需要将所有数字都放入内存中的连续块中。如果您正在处理结构和类中的字段,NEON无法真正帮助您。它并不是一种通用的"并行的东西"发动机。它是用于进行并行数学运算的SIMD处理器。
对于非常高性能的系统,数据格式就是一切。您不会采用任意数据格式(结构,类等)并尝试快速制作它们。你找出了可以让你做最平行工作的数据格式,并且你可以编写代码。您使数据连续。您不惜一切代价避免内存分配。但这并不是一个简单的StackOverflow问题可以解决的问题。高性能编程是一整套技能和不同的思考方式。找到合适的编译器标志并不是你得到的东西。正如您所发现的那样,默认值已经相当不错了。
您应该问的真正问题是,您是否可以重新组织数据,以便可以使用更多的OpenCV。 OpenCV已经有很多优化的并行操作,几乎可以肯定地充分利用NEON。您希望尽可能地保持OpenCV所用格式的数据。这可能是您获得最大改进的地方。
我的经验是,手写NEON程序集肯定有可能击败clang和gcc(至少从几年前开始,尽管编译器肯定会继续改进)。优秀的ARM优化与NEON优化不同。正如@Mats所指出的那样,编译器通常会在明显的情况下做得很好,但并不总是理想地处理每个案例,即使是一个技术娴熟的开发人员有时候也可能有时会击败它。 (@wallyk也是正确的,手动调整组件最好保存到最后;但它仍然可以非常强大。)
那就是说,鉴于你的声明"大会,我完全没有背景,并且在这一点上不可能学习,"那么不,你甚至不应该打扰。如果没有首先理解汇编的基础知识(以及一些非基础知识)(特别是矢量化的NEON汇编),那么对编译器进行二次猜测是没有意义的。击败编译器的第一步是了解目标。
如果您愿意学习目标,我最喜欢的介绍是Whirlwind Tour of ARM Assembly。那个,加上其他一些参考文献(下面),足以让我在我的特定问题中击败编译器2-3倍。另一方面,它们还不够,以至于当我向经验丰富的NEON开发人员展示我的代码时,他看了大约三秒钟并且说#34;你在那里停了下来。"真正好的装配很难,但是半合适的装配仍然比优化的C ++更好。 (同样,随着编译器编写者变得更好,每年都会变得不那么真实,但它仍然可以成为现实。)
一方面注意,my experience with NEON intrinsics是他们很少值得这么麻烦。如果您要打败编译器,那么您将需要实际编写完整程序集。大多数时候,无论你使用什么内在的东西,编译器都已经知道了。你获得权力的地方更多的是重组你的循环以最好地管理你的管道(内部人员不会帮助那里)。在过去的几年中,这种情况有所改善,但我希望改进的矢量优化器能够超越内在函数的价值而不是相反。
答案 1 :(得分:6)
这里也是"我也是"来自ARM的一些博客文章。 FIRST ,从以下内容开始获取背景信息,包括32位ARM(ARMV7及以下版本),Aarch32(ARMv8 32位ARM)和Aarch64(ARMv8 64) -bit ARM):
第二次 ,查看 NEON 系列编码。它是一个很好的图片介绍,所以像交错负载这样的东西一目了然。
我还去亚马逊寻找一些关于ARM装配的书籍,并对NEON进行了处理。我只能找到两个,这本书对NEON的处理都不令人印象深刻。他们用强制性的Matrix示例简化为一章。
我相信ARM内在函数是一个非常好的主意。 instrinsics允许您为GCC,Clang和Visual C / C ++编译器编写代码。我们有一个代码库适用于ARM Linux发行版(如Linaro),一些iOS设备(使用-arch armv7
)和Microsoft小工具(如Windows Phone和Windows Store应用程序)。
答案 2 :(得分:5)
除了Wally的答案 - 可能应该是一个评论,但我不能做得足够短:ARM有一个编译器开发团队,其整个角色是改进GCC和Clang / llvm的部分代码ARM CPU的生成,包括提供“自动矢量化”的功能 - 我没有深入研究它,但根据我在x86代码生成方面的经验,我期望任何相对容易矢量化的东西,编译器应该做一个深化工作。有些代码很难让编译器理解何时可以进行矢量化,也可能需要一些“鼓励” - 例如展开循环或将条件标记为“可能”或“不太可能”等等。
免责声明:我在ARM工作,但与编译器甚至CPU没什么关系,因为我为制作图形的组工作(我在GPU的OpenCL部分参与了GPU的编译器)驱动程序)。
修改强>
性能和各种指令扩展的使用实际上完全依赖于代码正在做什么。我希望像OpenCV这样的库已经在他们的代码中做了相当多的聪明的东西(例如手写汇编程序作为编译器内在函数和一般代码,旨在让编译器已经做好了),所以它可能不会给你太多改善。我不是计算机视觉专家,所以我无法真正评论OpenCV上完成了多少这样的工作,但我当然希望代码的“最热点”已经相当好地优化了。
另外,配置您的应用程序。不要只是摆弄优化标志,测量它的性能并使用分析工具(例如Linux“perf”工具)来衡量代码花费时间的地方。然后看看该特定代码可以做些什么。是否可以编写更平行的版本?编译器可以帮忙,你需要编写汇编程序吗?是否有不同的算法可以做同样的事情,但是以更好的方式等等......
尽管调整编译器选项可以提供帮助,但通常情况下,它可以提供几十个百分点,其中算法的更改通常会导致代码速度提高10倍或100倍 - 当然,假设您的算法可以改进!
然而,了解应用程序的哪个部分花费时间是关键。改变事物使得代码花费5%的时间快10%是没有意义的,当其他地方的改变可以使一段代码占总时间的30%或60%时快20%。或者优化一些数学例程,当80%的时间用于读取文件时,将缓冲区的大小增加两倍会使其快两倍......
答案 3 :(得分:5)
如果您可以使用合理的现代GCC(GCC 4.8及更高版本),我建议您使用内在函数。 NEON内在函数是编译器知道的一组函数,可以从C或C ++程序中使用它来生成NEON /高级SIMD指令。要在您的计划中访问它们,必须#include <arm_neon.h>
。有关所有可用内在函数的详细文档可在http://infocenter.arm.com/help/topic/com.arm.doc.ihi0073a/IHI0073A_arm_neon_intrinsics_ref.pdf获得,但您可以在其他地方找到更多用户友好的教程。
本网站上的建议通常是针对NEON内在函数的,当然有GCC版本在实现它们方面做得不好,但最近的版本做得相当好(如果你发现代码生成不好,请把它提升为一个错误 - https://gcc.gnu.org/bugzilla/)
它们是编程到NEON / Advanced SIMD指令集的简单方法,您可以实现的性能通常相当不错。它们也是“便携式”,因为当你转移到AArch64系统时,可以使用ARMv7-A中可以使用的内在函数的超集。它们还可以在ARM体系结构的各个实现中移植,这些实现的性能特征可能不同,但编译器将为性能调整建模。
NEON内在函数与手写汇编相比的主要优点是编译器在执行各种优化传递时可以理解它们。相比之下,手写汇编程序是GCC的不透明块,不会进行优化。另一方面,专家汇编程序员经常可以击败编译器的寄存器分配策略,特别是在使用写入或读取多个连续寄存器的指令时。
答案 4 :(得分:3)
虽然自从我提交这个问题已经过了很长时间,但我意识到它收集了一些兴趣,我决定告诉我最后做了些什么。
我的主要目标是优化for循环,这是项目的瓶颈。所以,既然我对大会一无所知,我决定给NEON内在论一个机会。 我最终获得了40-50%的性能提升(仅在此循环中),并且整个项目的整体性能得到了显着改善。
代码做了一些数学运算,将一堆原始距离数据转换为以毫米为单位的平面距离。我使用了一些未在此定义的常量(如_constant05,_fXtoZ),但它们只是其他地方定义的常量值。 正如您所看到的,我一次只为4个元素进行数学运算,讨论真正的并行化:)
unsigned short* frameData = frame.ptr<unsigned short>(_depthLimits.y, _depthLimits.x);
unsigned short step = _runWidth - _actWidth; //because a ROI being processed, not the whole image
cv::Mat distToPlaneMat = cv::Mat::zeros(_runHeight, _runWidth, CV_32F);
float* fltPtr = distToPlaneMat.ptr<float>(_depthLimits.y, _depthLimits.x); //A pointer to the start of the data
for(unsigned short y = _depthLimits.y; y < _depthLimits.y + _depthLimits.height; y++)
{
for (unsigned short x = _depthLimits.x; x < _depthLimits.x + _depthLimits.width - 1; x +=4)
{
float32x4_t projX = {(float)x, (float)(x + 1), (float)(x + 2), (float)(x + 3)};
float32x4_t projY = {(float)y, (float)y, (float)y, (float)y};
framePixels = vld1_u16(frameData);
float32x4_t floatFramePixels = {(float)framePixels[0], (float)framePixels[1], (float)framePixels[2], (float)framePixels[3]};
float32x4_t fNormalizedY = vmlsq_f32(_constant05, projY, _yResInv);
float32x4_t auxfNormalizedX = vmulq_f32(projX, _xResInv);
float32x4_t fNormalizedX = vsubq_f32(auxfNormalizedX, _constant05);
float32x4_t realWorldX = vmulq_f32(fNormalizedX, floatFramePixels);
realWorldX = vmulq_f32(realWorldX, _fXtoZ);
float32x4_t realWorldY = vmulq_f32(fNormalizedY, floatFramePixels);
realWorldY = vmulq_f32(realWorldY, _fYtoZ);
float32x4_t realWorldZ = floatFramePixels;
realWorldX = vsubq_f32(realWorldX, _tlVecX);
realWorldY = vsubq_f32(realWorldY, _tlVecY);
realWorldZ = vsubq_f32(realWorldZ, _tlVecZ);
float32x4_t distAuxX, distAuxY, distAuxZ;
distAuxX = vmulq_f32(realWorldX, _xPlane);
distAuxY = vmulq_f32(realWorldY, _yPlane);
distAuxZ = vmulq_f32(realWorldZ, _zPlane);
float32x4_t distToPlane = vaddq_f32(distAuxX, distAuxY);
distToPlane = vaddq_f32(distToPlane, distAuxZ);
*fltPtr = (float) distToPlane[0];
*(fltPtr + 1) = (float) distToPlane[1];
*(fltPtr + 2) = (float) distToPlane[2];
*(fltPtr + 3) = (float) distToPlane[3];
frameData += 4;
fltPtr += 4;
}
frameData += step;
fltPtr += step;
}
答案 5 :(得分:0)
在QEMU上玩一些最小的组装示例以了解说明
以下设置尚无很多示例,但可以用作整洁的操场:
示例在QEMU用户模式下运行,该模式分配了额外的硬件,并且GDB正常工作。
断言是通过C标准库完成的。
学习新的说明后,您应该能够轻松扩展该设置。
尤其是ARM intrinsincs在以下网址问:Is there a good reference for ARM Neon intrinsics?