手臂霓虹灯转置 4x4 uint32
Posted
技术标签:
【中文标题】手臂霓虹灯转置 4x4 uint32【英文标题】:arm neon transpose 4x4 uint32 【发布时间】:2016-04-23 20:24:57 【问题描述】:我正在尝试将图像逆时针旋转 90 度,然后水平翻转。
我的第一种方法是只使用 OpenCV:
cv::transpose(in, tmp); // transpose around top left
cv::flip(tmp, out, -1); // flip on both axes
为了性能,我试图将这两个函数合并为一个。
我的代码:
void ccw90_hflip_640x480(const cv::Mat& img, cv::Mat& out)
assert(img.cols == 640 && img.rows == 480);
assert(out.cols == 480 && out.cols == 640);
uint32_t* imgData = (uint32_t*)img.data;
uint32_t* outData = (uint32_t*)out.data;
uint32_t *pRow = imgData;
uint32_t *pEnd = imgData + (640 * 480);
uint32_t *dstCol = outData + (480 * 640) - 1;
for( ; pRow != pEnd; pRow += 640, dstCol -= 1)
for(uint32_t *ptr = pRow, *end = pRow + 640, *dst = dstCol;
ptr != end;
++ptr, dst -= 480)
*dst = *ptr;
我认为上述方法会更快,但事实并非如此。除了可能使用 NEON 的 OpenCV 之外,我想不出它不会更快的任何原因。
我找到了这篇文章/演示文稿: http://shervinemami.info/NEON_RotateBGRX.swf
转置和翻转以某种方式模糊在一起,这使得很难修改它以另一种方式旋转的位置,并且像我需要的那样围绕水平轴翻转。这篇文章很老了,所以我希望有一种更直接的方法来做我需要的事情。
那么使用 arm NEON 转置 4x4 uint32 矩阵的最简单方法是什么?
【问题讨论】:
确实,我已经穿越到了未来,在那里我确认我的 arm neon 实现速度更快。不幸的是,在我写下算法之前,我的时间机器发生了故障,把我送到了另一个宇宙,每个人都使用链表而不是向量。 请参阅here 了解解决方案(向下滚动查看实际有效的代码)。 @PaulR 感谢您的链接。我已经按照文章进行了转置,但是需要一些时间才能将完整的算法拼凑在一起并进行一些测量。 @PaulR 我最终使用了您推荐的文章中的第一种方法(vtrn,vtrn,vswp,vswp)。由于我正在使用的元素大小/计数,讨论的去交错加载是不可行的。在使用上述转置指令(和翻转,如上面链接的 *.swf 文件中所讨论的)更新上述算法以一次处理 4x4 像素后,我能够将运行时间减少到大约 30%。这使您的回答最正确,所以如果您发布答案,我会接受。谢谢。 很高兴知道它成功了 - 为了未来读者的利益,请随意写下这个作为自我回答(我现在在移动设备上,所以不适合写得到一个体面的答案)。 【参考方案1】:以下代码等同于原始帖子中的 OpenCV 调用,但执行速度要快几倍(至少在我的设备上)。
使用 Neon 确实显着提高了性能。由于转置发生在 CPU 内部,内存访问可以简化为以四个为一组读取和写入像素,而不是像 cmets 中讨论的那样一次一个。
void ccw90_hflip_640x480_neon(const cv::Mat& img, cv::Mat& out)
assert(img.cols == 640 && img.rows == 480);
assert(out.cols == 480 && out.cols == 640);
uint32_t *pRow = (uint32_t*)img.data;
uint32_t *pEnd = (uint32_t*)img.data + (640 * 480);
uint32_t *dstCol = (uint32_t*)out.data + (480 * 640) - (480 * 3) - 4;
for( ; pRow != pEnd; pRow += 640 * 4, dstCol -= 4)
for(uint32_t *ptr = pRow, *end = pRow + 640, *dst = dstCol;
ptr != end;
ptr += 4, dst -= 480 * 4)
uint32_t* in0 = ptr;
uint32_t* in1 = in0 + 640;
uint32_t* in2 = in1 + 640;
uint32_t* in3 = in2 + 640;
uint32_t* out0 = dst;
uint32_t* out1 = out0 + 480;
uint32_t* out2 = out1 + 480;
uint32_t* out3 = out2 + 480;
asm("vld1.32 d0, d1, [%[in0]] \n"
"vld1.32 d2, d3, [%[in1]] \n"
"vld1.32 d4, d5, [%[in2]] \n"
"vld1.32 d6, d7, [%[in3]] \n"
"vtrn.32 q0, q1 \n"
"vtrn.32 q2, q3 \n"
"vswp d1, d4 \n"
"vswp d3, d6 \n"
"vrev64.32 q0, q0 \n"
"vrev64.32 q1, q1 \n"
"vrev64.32 q2, q2 \n"
"vrev64.32 q3, q3 \n"
"vswp d0, d1 \n"
"vswp d2, d3 \n"
"vswp d4, d5 \n"
"vswp d6, d7 \n"
"vst1.32 d6, d7, [%[out0]] \n"
"vst1.32 d4, d5, [%[out1]] \n"
"vst1.32 d2, d3, [%[out2]] \n"
"vst1.32 d0, d1, [%[out3]] \n"
:
: [out0] "r" (out0), [out1] "r" (out1), [out2] "r" (out2), [out3] "r" (out3),
[in0] "r" (in0), [in1] "r" (in1), [in2] "r" (in2), [in3] "r" (in3)
: "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7"
);
【讨论】:
我无法在 gcc 编译器上编译这个函数。我收到错误消息 - “错误:未知助记符vld1.32' --
vld1.32 d0,d1,[x4]'”
你启用了neon指令集吗?你的目标是 32 位吗? arm64 使用不同的指令集。
我有 arm64 处理器。您能否分享一个链接,我可以在其中找到 arm64 指令集的示例。
不幸的是,这不是直接的 1:1 翻译。除了指令名称之外,还有其他一些小的差异。您必须阅读并学习 arm64 的基础知识。如果你不想这样做,你总是可以尝试内在函数(它会自动生成 32 位和 64 位程序集),看看你是否能获得足够的速度。【参考方案2】:
霓虹灯不会有很大帮助注意。您的代码只是在移动数据;霓虹灯不能使底层内存显着加快。见this article;使用 PLD 也会有所帮助。我建议您按顺序处理dst
,然后使用ptr
跳转。缓存将预填充ptr
和dst
将行填充。
这是遍历内存的另一种形式(变量名可能没有意义),
uint32_t *pEnd = imgData + 640;
uint32_t *dstCol = outData;
for( ; pRow != pEnd; pRow ++)
for(uint32_t *ptr = pRow, *dst = dstCol, *end = dst + 480;
dst != end;
ptr += 640, dst++)
*dst = *ptr;
// could flush `dstCol` here as it is complete or hope the system clues in.
dstCol += 480;
这个想法是按顺序填写dst
,然后跳转访问imgData
。如果你按顺序写出来,所有现代记忆都会更有效地填充。高速缓存和同步 DRAM 通常一次填充几个 32 位字。我们可以利用 L1 缓存的知识展开内部循环。它是 32 或 64 字节,代表 8 或 16 个 32 位像素。填充量将相似,因此您可以将转置减少到 cacheable 块并一次处理每个块。将 640x480 图像视为由 8*8 像素图块(最小 L1 缓存大小)组成,并依次处理每个图块。
执行此操作后,NEON 指令可能会增加一些百分比。但是,优化加载/存储单元(所有 CPU 单元通用)应该是第一步。
注意:Neon 是 SIMD(单指令,多数据),它擅长对像素进行数字处理,通过一次处理多个像素来提高计算能力。它确实有一些指令可以优化内存遍历,但核心 CPU 单元和 SIMD/NEON 单元的底层内存是相同的。 NEON 可能会带来提升,但我认为在您优化内存系统的访问顺序之前这是徒劳的。
【讨论】:
我所说的对于较大的图像会更明显,如果您只处理小块,则更少。请注意,Paul Rs NEON link 有 Peter Harris 提供的有关使用 NEON 处理 16x16 瓷砖的重要信息。把这些和上面的信息结合起来,你可能会有一个接近最佳的例程。 嗯,最初的推理对我来说似乎有悖常理——存储缓冲区、L1 和 L2 总是会吸收一些存储的开销,而如果 L1 的负载丢失,你就没有什么可做的了做,但等待。允许存储缓冲区更有效地合并写入,代价是几乎保证在 L1 上错过每个负载(在没有非常仔细的预取的情况下),这似乎不是什么胜利,但我承认这是不是我实际进行过基准测试的东西(显然很大程度上取决于系统特定的因素)。 @Notlikethat 是的,存储缓冲区将用于顺序写入并且大小有限。 L1 将在初始读取时丢失,但它是 4way 至少 32k。步幅只有 480 个条目,因此读取的第二列将在缓存中。但是,我建议 OP 尝试一下。只有通过测量才能找到确定的答案。当然,平铺方法会更好。这取决于整个内存系统,但我很确定在一大类 CPU 上,原始算法正在执行更多的物理写入;而第二个可能会有相同的读取。 @artlessnoise 这很有趣。我不知道写入也会受到缓存性能的影响,这就是我为有序读取编写代码的原因。我仍然会尝试使用 neon 优化,希望读取和写入都能体验到一些缓存优势。 正如主帖下方的 cmets 所述,使用霓虹灯一次处理 4x4 像素会带来巨大的性能提升。一旦我用 neon 修改了我的原始算法,我还测试了第二个版本,它针对顺序写入而不是您建议的顺序读取进行了优化。性能相似,但使用顺序写入似乎慢了约 25%。我还阅读了一些关于 PLD 的信息,似乎它可以进一步提高性能,但我已经实现的性能改进将让我们暂时摆脱困境。谢谢以上是关于手臂霓虹灯转置 4x4 uint32的主要内容,如果未能解决你的问题,请参考以下文章