背景(如果你愿意,跳过这个)
首先让我说我不是专家程序员.我是一名年轻的初级计算机视觉(CV)工程师,我在C++编程方面经验丰富,主要是因为大量使用了优秀的OpenCV2 C++ API.我所学到的只是通过执行项目的需要,解决问题和满足最后期限的需要,因为这是行业的现实.
最近,我们开始为嵌入式系统(ARM板)开发CV软件,我们使用普通的C++优化代码.然而,与传统计算机相比,由于其有限的资源,在这种架构中构建实时CV系统是一个巨大的挑战.
那是我发现NEON的时候.我已经阅读了很多关于此的文章,但这是一个相当新的主题,因此没有太多关于它的信息,我阅读的越多,我得到的就越混乱.
问题
我正在寻找使用NEON功能优化C++代码(主要是一些用于循环),一次计算4或8个数组元素.是否有某种库或一组函数可以在C++环境中使用?我混淆的主要原因是我看到的几乎所有代码snipets都在Assembly中,我完全没有背景,而且在这一点上也不可能学习.我在Linux Gentoo中使用Eclipse IDE来编写C++代码.
UPDATE
看完答案后,我用软件做了一些测试.我使用以下标志编译了我的项目:
-O3 -mcpu=cortex-a9 -ftree-vectorize -mfloat-abi=hard -mfpu=neon
Run Code Online (Sandbox Code Playgroud)
请记住,这个项目包括大量的库,如openframeworks,OpenCV和OpenNI,所有内容都是用这些标志编译的.为了编译ARM板,我们使用Linaro工具链交叉编译器,GCC的版本是4.8.3.您是否希望这可以改善项目的性能?因为我们没有经历任何变化,考虑到我在这里阅读的所有答案,这是相当奇怪的.
另一个问题:所有for循环都有明显的迭代次数,但其中许多循环遍历自定义数据类型(结构或类).GCC能否优化这些周期,即使它们遍历自定义数据类型?
Rob*_*ier 14
编辑:
从您的更新中,您可能会误解NEON处理器的功能.它是SIMD(单指令,多数据)矢量处理器.这意味着它非常擅长同时对几个数据执行指令(比如说"乘以4").它还喜欢做"将所有这些数字加在一起"或"添加这两个数字列表中的每个元素以创建第三个数字列表"之类的事情.因此,如果你看起来像问题,NEON处理器将是巨大的帮助.
为了获得这些好处,您必须以非常特定的格式存储数据,以便向量处理器可以同时加载多个数据,并行处理,然后同时将其写回.你需要组织一些事情,使数学避免大多数条件(因为过早查看结果意味着往返NEON).矢量编程是一种不同的思考方案的方式.这都是关于管道管理的.
现在,对于许多非常常见的问题,编译器会自动完成所有这些工作.但它仍然在处理数字和特定格式的数字.例如,您几乎总是需要将所有数字都放入内存中的连续块中.如果您正在处理结构和类中的字段,NEON无法真正帮助您.它不是通用的"并行做事"引擎.它是一个用于并行数学运算的SIMD处理器.
对于非常高性能的系统,数据格式就是一切.您不会采用任意数据格式(结构,类等)并尝试使它们快速.你找出了可以让你做最平行工作的数据格式,并且你可以编写代码.您使数据连续.您不惜一切代价避免内存分配.但这并不是一个简单的StackOverflow问题可以解决的问题.高性能编程是一整套技能和不同的思考方式.通过找到正确的编译器标志,这不是你得到的.正如您所发现的那样,默认值已经相当不错了.
您应该问的真正问题是,您是否可以重新组织数据,以便可以使用更多的OpenCV.OpenCV已经有很多优化的并行操作,几乎可以肯定地充分利用NEON.您希望尽可能地保持OpenCV工作格式的数据.这可能是您获得最大改进的地方.
我的经验是,手写NEON程序集肯定有可能击败clang和gcc(至少从几年前开始,尽管编译器肯定会继续改进).优秀的ARM优化与NEON优化不同.正如@Mats所指出的那样,编译器通常会在明显的情况下做得很好,但并不总是理想地处理每个案例,即使是一个技术娴熟的开发人员有时候也可能有时会打败它.(@wallyk也是正确的,手动调整组件最好保存到最后;但它仍然可以非常强大.)
也就是说,鉴于你的声明"大会,我完全没有背景,在这一点上不可能学习",那么不,你甚至不应该打扰.如果没有首先理解汇编的基础知识(以及一些非基础知识)(特别是矢量化的NEON汇编),那么对编译器进行二次猜测是没有意义的.击败编译器的第一步是知道目标.
如果您愿意学习目标,我最喜欢的介绍是ARM Assembly的Whirlwind Tour.那个,加上其他一些参考文献(下面),足以让我在我的特定问题中击败编译器2-3倍.另一方面,它们还不够,以至于当我向经验丰富的NEON开发人员展示我的代码时,他看了大约三秒钟并且说"你在那里停了下来".真正好的装配很难,但是半合适的装配仍然比优化的C++更好.(同样,随着编译器编写者变得更好,每年都会变得不那么真实,但它仍然可以成为现实.)
另一方面,我对NEON内在函数的体验是,他们很少值得这么麻烦.如果您要打败编译器,那么您将需要实际编写完整程序集.大多数时候,无论你使用什么内在的东西,编译器都已经知道了.您获得权力的地方更多的是重组您的循环以最好地管理您的管道(并且内在函数在那里没有帮助).在过去的几年中,这种情况有可能得到改善,但我希望改进的矢量优化器能够超越内在函数的价值而不是相反.
这里有来自ARM的一些博客文章的"mee too".FIRST,启动用下面来获得背景信息,包括32位的ARM(ARMv7的及以下),Aarch32(ARMv8 32位ARM)和Aarch64(ARMv8 64位ARM):
其次,检查 NEON系列的编码.它是一个很好的图片介绍,所以像交错负载这样的东西一目了然.
我还去亚马逊寻找一些有关NEON处理的ARM装配书籍.我只能找到两个,这本书对NEON的处理都不令人印象深刻.他们用强制性的Matrix示例简化为一章.
我相信ARM Intrinsics是一个非常好的主意.instrinsics允许您为GCC,Clang和Visual C/C++编译器编写代码.我们有一个适用于ARM Linux发行版(如Linaro),一些iOS设备(使用-arch armv7)和Microsoft小工具(如Windows Phone和Windows Store应用程序)的代码库.
除了Wally的回答 - 也许应该是一个评论,但我不能做得足够短:ARM有一个编译器开发团队,其整个角色是改进GCC的部分和为ARM进行代码生成的Clang/llvm CPU,包括提供"自动矢量化"的功能 - 我没有深入研究它,但根据我在x86代码生成方面的经验,我期望任何相对容易矢量化的东西,编译器应该做一个很好的工作.有些代码很难让编译器理解何时可以进行矢量化,也可能需要一些"鼓励" - 例如展开循环或将条件标记为"可能"或"不太可能"等.
免责声明:我在ARM工作,但与编译器甚至CPU没什么关系,因为我为图形组工作(我在GPU驱动程序的OpenCL部分中涉及GPU的编译器).
编辑:
性能和各种指令扩展的使用实际上完全取决于代码的作用.我希望像OpenCV这样的库已经在他们的代码中做了相当多的聪明的东西(例如手写汇编程序作为编译器内在函数和一般代码,旨在让编译器已经做好了),所以它可能不会给你太多改进.我不是计算机视觉专家,所以我无法真正评论OpenCV上完成了多少这样的工作,但我当然希望代码的"最热点"已经相当好地优化了.
另外,描述您的应用程序.不要只是摆弄优化标志,测量它的性能并使用分析工具(例如Linux"perf"工具)来衡量代码花费时间的地方.然后看看该特定代码可以做些什么.是否可以编写更平行的版本?编译器可以帮忙,你需要编写汇编程序吗?是否有不同的算法可以做同样的事情,但是以更好的方式等等...
虽然调整编译器选项可以提供帮助,但通常情况下,它可以提供几十个百分点,其中算法的更改通常可以使代码快10倍或100倍 - 当然,假设您的算法可以得到改进!
但是,了解应用程序的哪个部分需要花费时间.改变事物使得代码花费5%的时间快10%是没有意义的,当其他地方的改变可以使一段代码占总时间的30%或60%时快20%.或者优化一些数学例程,当80%的时间花在读取文件上时,使缓冲区大小增加两倍会使其快两倍...
如果您可以使用合理的现代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版本在实现它们方面做得不好,但最近的版本做得相当好(如果你发现代码生成不好,请把它作为一个bug提出来 - https://gcc.gnu.org/bugzilla/)
它们是编程到NEON/Advanced SIMD指令集的简单方法,您可以实现的性能通常相当不错.它们也是"可移植的",因为当你转移到AArch64系统时,可以使用ARMv7-A中可以使用的内在函数的超集.它们也可以在ARM体系结构的各个实现中移植,这些实现可以在性能特征上有所不同,但编译器将为性能调整建模.
NEON内在函数与手写汇编相比的主要优点是编译器在执行各种优化过程时可以理解它们.相比之下,手写汇编程序是GCC的不透明块,不会进行优化.另一方面,专家汇编程序员经常可以击败编译器的寄存器分配策略,特别是在使用写入或读取多个连续寄存器的指令时.
虽然我提交这个问题已经过去了很长时间,但我意识到它引起了一些兴趣,我决定告诉我我最终做了什么。
我的主要目标是优化作为项目瓶颈的 for 循环。因此,由于我对 Assembly 一无所知,因此我决定尝试使用 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;
}
Run Code Online (Sandbox Code Playgroud)