ARM NEON编码:如何启动

Coding for ARM NEON: How to start?

本文关键字:启动 何启动 NEON 编码 ARM      更新时间:2023-10-16

我希望使用一次计算4或8个数组元素的NEON功能来优化C++代码(主要是循环的一些)。是否有某种库或函数集可以在C++环境中使用?

我在Linux Gentoo中使用Eclipse IDE来编写C++代码。

更新

看完答案后,我用软件做了一些测试。我用以下标志编译了我的项目:

-O3 -mcpu=cortex-a9 -ftree-vectorize -mfloat-abi=hard -mfpu=neon 

请记住,这个项目包括大量的库,如开放框架、OpenCV和OpenNI,所有的东西都是用这些标志编译的。

为了编译ARM板,我们使用Linaro工具链交叉编译器,GCC的版本是4.8.3。

你希望这能提高项目的绩效吗?因为我们没有经历任何变化,考虑到我在这里读到的所有答案,这很奇怪。

另一个问题是:循环的所有都有明显的迭代次数,但其中许多都是通过自定义数据类型(结构或类)进行迭代的。GCC是否可以优化这些循环,即使它们通过自定义数据类型进行迭代?

编辑:

从您的更新来看,您可能会误解NEON处理器的功能。它是一个SIMD(单指令多数据)矢量处理器。这意味着它非常擅长同时对多条数据执行指令(比如"乘4")。它还喜欢做一些事情,比如"把所有这些数字加在一起"或"把这两个数字列表中的每个元素加起来,创建第三个数字列表。"因此,如果你的问题看起来像这些,NEON处理器将提供巨大帮助。

为了获得这一好处,您必须将数据放在非常特定的格式中,以便矢量处理器可以同时加载多个数据,并行处理,然后同时将其写回。你需要把事情组织起来,这样数学就可以避免大多数条件语句(因为过早地查看结果意味着要往返于NEON)。矢量编程是一种不同的程序思维方式。这一切都与管道管理有关。

现在,对于许多非常常见的问题,编译器可以自动解决所有这些问题。但它仍然是关于处理数字,以及特定格式的数字。例如,您几乎总是需要将所有数字放入内存中的一个连续块中。如果你在处理结构和类内部的字段,NEON并不能真正帮助你。它不是一个通用的"并行处理"引擎。这是一个用于进行并行数学的SIMD处理器。

对于非常高性能的系统,数据格式就是一切。您不需要采用任意的数据格式(结构、类等)并试图使其快速。你可以找到可以让你做最多并行工作的数据格式,并围绕它编写代码。您使数据连续。您可以不惜一切代价避免内存分配。但这并不是一个简单的StackOverflow问题所能解决的问题。高性能编程是一整套技能,也是一种不同的思考方式。这不是通过找到正确的编译器标志就能得到的。正如您所发现的,默认值已经相当不错了。

你应该问的真正问题是,你是否可以重新组织你的数据,以便你可以使用更多的OpenCV。OpenCV已经有很多优化的并行操作,这些操作几乎肯定会很好地利用NEON。尽可能地,你想让你的数据保持OpenCV工作的格式。这可能是你将获得最大改进的地方。


我的经验是,手工编写NEON程序集肯定是可能的,它将击败clang和gcc(至少从几年前开始,尽管编译器肯定在不断改进)。拥有优秀的ARM优化并不等同于NEON优化。正如@Mats所指出的,编译器通常会在明显的情况下做得很好,但并不总是理想地处理每一个情况,即使是技术不熟练的开发人员也有可能有时甚至戏剧性地击败它。(@wallyk也正确地认为,手动调谐组件最好留到最后;但它仍然可以非常强大。)

也就是说,考虑到你的声明"大会,我完全没有背景,在这一点上不可能学习",那么不,你甚至不应该麻烦。如果不首先至少了解汇编(特别是矢量化的NEON汇编)的基础知识(以及一些非基础知识),就没有必要对编译器进行事后猜测。击败编译器的第一步是知道目标。

如果你愿意学习目标,我最喜欢的介绍是ARM组装的旋风之旅。这一点,加上其他一些参考(如下),足以让我在特定问题上以2-3x的优势击败编译器。另一方面,它们还不够,以至于当我向一位经验丰富的NEON开发人员展示我的代码时,他看了大约三秒,说"你就在那里停了下来。"真正好的汇编很难,但一半好的汇编仍然可以比优化的C++更好。(同样,随着编译器编写人员的进步,这种情况每年都会变得不那么真实,但它仍然可能是真实的。)

  • ARM汇编语言
  • iOS开发人员应该了解ARM架构的一些内容(以iPhone为重点,但所有用途的原理都是一样的)
  • ARM编译器中的ARM NEON支持
  • 近地天体编码

有一点需要注意,我对NEON内部的经验是,它们很少值得麻烦。如果你要打败编译器,你需要真正编写完整的程序集。大多数时候,无论你使用什么内在的,编译器都已经知道了。你的力量更多的是重组你的循环,以最好地管理你的管道(本质并没有帮助)。这可能在过去几年中有所改进,但我预计改进后的向量优化器将超过内部函数的值。

这里有一个"mee too",包含ARM的一些博客文章FIRST,从以下内容开始获取后台信息,包括32位ARM(ARMV7及以下版本)、Aarch32(ARMv8 32位ARM)和Aarch64(ARMv864位ARM):

  • ARM NEON编程快速参考

第二次 ,检查NEON系列的编码。这是一个很好的图片介绍,所以像交错加载这样的东西一看就有意义。

  • ARM NEON编程快速参考

  • 近地天体编码.第1部分:加载和存储

  • 近地天体编码-第2部分:处理遗留

  • 近地天体编码.第3部分:矩阵乘法

  • 近地天体编码-第4部分:左移和右移

  • 近地天体编码.第5部分:重新排列矢量

我还在亚马逊上寻找一些关于ARM组装和NEON治疗的书籍。我只能找到两本,这两本书对近地天体的处理都不令人印象深刻。他们简化为一个单独的章节,带有强制性的矩阵示例。


我相信ARM Intrnsics是一个非常好的主意。instrinsic允许您为GCC、Clang和Visual C/C++编译器编写代码。我们有一个适用于ARM Linux发行版(如Linaro)、一些iOS设备(使用-arch armv7)和Microsoft小工具(如Windows Phone和Windows应用商店)的代码库。

如果您可以访问一个相当现代的GCC(GCC 4.8及更高版本),我建议您尝试使用内部函数。NEON内部函数是编译器知道的一组函数,可以从C或C++程序中使用这些函数来生成NEON/Advanced 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架构的实现中移植,ARM架构的性能特征可能有所不同,但编译器将对其进行建模以进行性能调整。

与手工编写的程序集相比,NEON内部函数的主要优点是编译器在执行各种优化过程时可以理解它们。相比之下,手工编写的汇编程序对GCC来说是一个不透明的块,不会进行优化。另一方面,专业的汇编程序程序员通常可以击败编译器的寄存器分配策略,尤其是在使用向多个连续寄存器写入或从中读取的指令时。

除了Wally的回答之外-可能应该是一个评论,但我不能把它说得不够简短:ARM有一个编译器开发团队,他们的全部职责是改进GCC和Clang/llvm中为ARM CPU生成代码的部分,包括提供"自动矢量化"的功能-我还没有深入研究它,但根据我在x86代码生成方面的经验,我认为对于任何相对容易向量化的东西,编译器都应该做得很好。编译器很难理解某些代码何时可以向量化,并且可能需要一些"鼓励",例如展开循环或将条件标记为"可能"或"不可能"等。

免责声明:我为ARM工作,但与编译器甚至CPU几乎没有关系,因为我为图形团队工作(我在GPU驱动程序的OpenCL部分参与了GPU的编译器)。

编辑:

性能和各种指令扩展的使用实际上取决于代码在做什么。我预计像OpenCV这样的库已经在它们的代码中做了相当多的聪明的事情(比如作为编译器内部的手写汇编程序,以及通常设计为允许编译器已经做得很好的代码),所以它可能不会真正给你带来太多改进。我不是计算机视觉专家,所以我真的不能评论在OpenCV上到底做了多少这样的工作,但我当然希望代码的"最热门"点已经得到了相当好的优化。

此外,还可以对您的应用程序进行配置。不要只是摆弄优化标志,测量它的性能,并使用评测工具(例如Linux"perf"工具)来测量代码在哪里花费的时间。然后看看可以对那个特定的代码做些什么。有可能写一个更并行的版本吗?编译器能帮上忙吗?你需要写汇编程序吗?有没有一种不同的算法可以做同样的事情,但以更好的方式,等等…

尽管调整编译器选项会有所帮助,而且通常会有所帮助。但它可以提供数十%的速度,其中算法的变化通常会导致代码速度提高10倍或100倍——当然,假设你的算法可以得到改进!

然而,了解应用程序的哪个部分需要花费时间是关键。当其他地方的更改可能会使一段占总时间30%或60%的代码快20%时,更改内容以使占用5%时间的代码快10%是没有意义的。或者优化一些数学例程,当80%的时间都花在读取文件上时,将缓冲区大小增加一倍会使其速度增加一倍。。。

尽管我提交这个问题已经过去了很长时间,但我意识到它引起了一些兴趣,我决定告诉大家我最终对此做了什么。

我的主要目标是优化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;
}

如果您根本不想处理汇编代码,那么调整编译器标志以最大限度地优化速度。给定适当ARM目标的gcc应该这样做,前提是循环迭代次数是明显的。

要检查gcc代码生成,请通过添加-S标志来请求程序集输出。

如果经过几次尝试(阅读gcc文档和调整标志)仍然无法让它生成您想要的代码,那么就获取程序集输出并编辑它以使您满意。


小心过早优化。正确的开发顺序是让代码发挥作用,然后看看它是否需要优化。只有当代码稳定时,这样做才有意义

在QEMU上玩一些最小的组装示例来理解说明

下面的设置还没有很多例子,但它是一个整洁的游乐场:

  • v7示例
  • v8示例
  • 设置使用情况

示例在QEMU用户模式下运行,该模式分配了额外的硬件,GDB运行得很好。

断言是通过C标准库完成的。

你应该能够在学习的过程中使用新的说明轻松地扩展设置。

特别是ARM内部插件被问到:ARM Neon内部插件有好的参考吗?