c++ ARM neon 编码:怎么下手?

sr4lhrrt  于 2022-11-19  发布在  其他
关注(0)|答案(7)|浏览(329)

我想优化C代码(主要是一些 *for循环 *),使用 neon 一次计算4或8个数组元素的能力。有没有某种库或函数集可以在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。
你希望这能提高项目的性能吗?因为我们没有经历任何变化,考虑到我在这里读到的所有答案,这是相当奇怪的。
另一个问题:所有的 *for循环 * 都有明显的迭代次数,但其中许多循环都是通过自定义数据类型(结构或类)来迭代的。2 GCC是否可以优化这些循环,即使它们是通过自定义数据类型来迭代的呢?

mf98qq94

mf98qq941#

编辑:
根据您的更新,您可能会误解 neon 处理器的功能。它是SIMD(单指令、多数据)向量处理器。这意味着它非常擅长执行指令(比如“乘4”)同时对几段数据。它还喜欢做“把所有这些数字加在一起”或“将这两个数字列表的每个元素相加,创建第三个数字列表。”因此,如果您的问题看起来像这些东西, neon 处理器将是巨大的帮助。
为了获得这种好处,必须将数据以非常特定的格式放置,以便矢量处理器可以同时加载多个数据,并行处理它,然后同时把它写出来。你需要组织好这些东西,这样数学就避免了大多数条件(因为过早地查看结果意味着往返于 neon )。向量编程是一种不同的程序思考方式。它It“这都是关于管道管理。
现在,对于许多非常常见的问题,编译器可以自动地解决所有这些问题。但它仍然是关于处理数字,以及特定格式的数字。例如,你几乎总是需要将所有的数字放入内存中的一个连续块中。如果你要处理结构体和类中的字段, neon 并不能真正帮助你。它不是一个通用的“并行处理”引擎。它是一个用于并行数学的SIMD处理器。
对于非常高性能的系统,数据格式就是一切。(结构体、类等)并尝试使它们更快。你弄清楚将让你做最多并行工作的数据格式,然后编写代码,使数据连续,不惜一切代价避免内存分配,但这并不是简单的StackOverflow问题所能解决的。高性能编程是一整套技能和一种不同的思考方式。它不是你通过找到正确的编译器标志就能得到的。正如你所发现的,默认值已经很好了。
你应该问的真实的问题是,你是否可以重新组织你的数据,以便你可以更多地使用OpenCV。OpenCV已经有了大量优化的并行操作,几乎肯定会充分利用 neon 。你希望尽可能多地以OpenCV工作的格式保存你的数据。这可能是你将获得最大改进的地方。
我的经验是,它肯定是可能的手写 neon 汇编,将击败clang和gcc(至少从几年前开始,尽管编译器肯定会继续改进)。拥有出色的ARM优化并不等同于 neon 优化。正如@Mats所指出的,编译器通常会在明显的情况下做得很好,但并不总是理想地处理每一种情况,甚至对于一个不太熟练的开发人员来说,有时候也确实有可能击败它,有时候甚至是戏剧性地击败它。(wallyk也是正确的,手工调整组件最好留到最后;但它仍然非常强大。)
也就是说,考虑到你的陈述“汇编,我完全没有背景知识,现在也不可能学得起”,那么不,你甚至不应该费心。如果没有首先至少了解汇编(特别是矢量化的 neon 汇编)的基础知识(和一些非基础知识),就没有必要对编译器进行事后猜测。击败编译器的第一步是知道目标。
如果你愿意学习目标,我最喜欢的介绍是Whirlwind Tour of ARM Assembly。(如下所示),足以让我在我的特定问题上击败编译器2- 3倍。另一方面,它们还不够,当我向一位有经验的 neon 开发人员展示我的代码时,他看了大约3秒钟,然后说:“你在这里停了下来。”真正好的汇编是很难的,但是稍微像样的汇编仍然可以比优化的C++更好。(同样,随着编译器编写者的进步,这一点每年都会变得不那么正确,但它仍然可能是正确的。)

顺便说一句,我对 neon 内部函数的经验是,它们很少值得这么麻烦。如果你想击败编译器,你需要实际编写完整的汇编。大多数时候,不管你使用什么内部函数,编译器已经知道了。你的能力更多的是在重新构造你的循环,以最好地管理你的流水线(而内部函数在这方面没有帮助)。在过去的几年里,这可能有所改进,但我希望改进的矢量优化器比内部函数的价值更大。

ljo96ir5

ljo96ir52#

以下是ARM的一些博客文章。首先,请从以下内容开始了解背景信息,包括32位ARM(ARMV7及以下版本)、Aarch 32(ARMv 8 32位ARM)和Aarch 64(ARMv 8 64位ARM):

我还在亚马逊上找了一些关于ARM组装的书,其中有关于 neon 的论述,但我只找到了两本,而且这两本书对NEON的论述都不令人印象深刻,它们都缩减到了一章,只提供了强制性的Matrix示例。
我相信ARM Intrinsics是一个非常好的想法。Intrinsics允许你为GCC、Clang和Visual C/C++编译器编写代码。我们有一个代码库,适用于ARM Linux发行版(如Linaro)、一些iOS设备(使用-arch armv7)和微软小工具(如Windows Phone和Windows Store Apps)。

ugmeyewa

ugmeyewa3#

如果你有一个相当现代的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 /高级SIMD指令集编程方式,您可以实现的性能通常相当不错。它们也是“可移植的”,因为当您移动到Aarch 64系统时,您可以使用ARMv7-A中的一个超集。它们还可以在ARM架构的实现中移植,这些实现的性能特征可能会有所不同,但编译器将为性能调整而对其建模。
neon 内部函数相对于手工编写的汇编程序的主要优点是,编译器在执行各种优化过程时可以理解它们。相反,手工编写的汇编程序对GCC是一个不透明的块,不会被优化。另一方面,专业的汇编程序员通常可以击败编译器的寄存器分配策略。特别是当使用写入或读取多个连续寄存器的指令时。

eyh26e7m

eyh26e7m4#

除了沃利的回答--而且大概应该是一句评语,但我怎么也说不出够简短的话:ARM有一个编译器开发团队,其全部职责是改进GCC和Clang/llvm中为ARM CPU生成代码的部分,包括提供“自动矢量化”的特性--我还没有深入研究过它,但根据我在x86代码生成方面的经验,我希望所有相对容易矢量化的功能都能实现。编译器应该做好充分的工作。2有些代码对于编译器来说很难理解它何时可以向量化,并且可能需要一些“鼓励”--比如展开循环或者将条件标记为“可能”或“不可能”等等。
免责声明:我为ARM工作,但与编译器甚至CPU关系不大,因为我为图形团队工作(在GPU驱动程序的OpenCL部分,我与GPU的编译器有一些联系)。

编辑:

性能和各种指令扩展的使用实际上完全取决于代码在做什么。我希望像OpenCV这样的库已经在它们的代码中做了相当多的聪明的事情(诸如作为编译器内部函数的手写汇编程序以及被设计为允许编译器已经做好工作的一般代码),因此它可能不会给您带来太多改进。我不是计算机视觉Maven,所以我不能确切地评论OpenCV上做了多少这样的工作,但我肯定希望代码的“最热”点已经得到了相当好的优化。
另外,分析你的应用程序。不要只是摆弄优化标志,测量它的性能,并使用分析工具(例如Linux的“perf”工具)来衡量你的代码在哪里花费了时间。然后看看可以对那个特定的代码做些什么。有没有可能写一个更并行的版本?编译器能帮上忙吗?你需要写汇编程序吗?有没有不同的算法做同样的事情,但以更好的方式,等等,等等。
虽然调整编译器选项可以帮助,而且经常这样做,它可以给予百分之几十,其中算法的变化往往可以导致10倍或100倍的代码快-当然,假设您的算法可以改进!
然而,了解应用程序的哪一部分花费了时间是关键。如果只需要改变某个地方,就可以使一段花费了5%时间的代码快上10%,而其他地方的改变却可以使一段花费了30%或60%时间的代码快上20%,那么改变一些东西就没有意义了。或者优化一些数学例程,当80%的时间花在阅读文件上时,将缓冲区的大小增加两倍会使其速度提高一倍...

gblwokeq

gblwokeq5#

虽然很长一段时间过去了,因为我提交了这个问题,我意识到它聚集了一些兴趣,我决定告诉我最终做了什么关于这一点。
我的主要目标是优化一个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;
}
rryofs0p

rryofs0p6#

如果您根本不想弄乱汇编代码,则调整编译器标志以最大限度地优化速度。gcc在给定适当ARM目标的情况下应该这样做,前提是循环迭代的数量是明显的。
若要检查gcc程式码产生,请加入-S旗标以要求组件输出。
如果经过几次尝试(阅读gcc文档和调整标志)仍然不能让它产生您想要的代码,那么就获取程序集输出并对其进行编辑,直到您满意为止。
注意 * 过早的优化 *。正确的开发顺序是让代码发挥作用,然后看看它是否 * 需要 * 优化。只有当代码稳定时,这样做才有意义。

7fyelxc5

7fyelxc57#

在QEMU上玩一些最小的汇编示例,以理解说明

下面的设置没有太多示例,但它可以作为一个整洁的练习场:

这些示例在QEMU用户模式下运行,这样就不需要额外的硬件,GDB运行得很好。
Assert是通过C标准库完成的。
您应该能够轻松地扩展该设置与新的指示,因为你学习他们。
特别是ARM内部培训,请访问:Is there a good reference for ARM Neon intrinsics?

相关问题