使用 ARM SIMD 指令优化掩码功能
Posted
技术标签:
【中文标题】使用 ARM SIMD 指令优化掩码功能【英文标题】:Optimizing mask function with ARM SIMD instructions 【发布时间】:2014-05-13 14:03:34 【问题描述】:我想知道您是否可以帮助我使用 NEON 内在函数来优化此遮罩功能。我已经尝试使用 O3 gcc 编译器标志使用自动矢量化,但该函数的性能比使用 O2 运行它要小,O2 会关闭自动矢量化。由于某种原因,使用 O3 生成的汇编代码比使用 O2 生成的汇编代码长 1.5。
void mask(unsigned int x, unsigned int y, uint32_t *s, uint32_t *m)
unsigned int ixy;
ixy = xsize * ysize;
while (ixy--)
*(s++) &= *(m++);
可能我必须使用以下命令:
vld1q_u32 // 从 s 和 m 加载 4 个整数
vandq_u32 // 在 s 和 m 的 4 个整数之间执行逻辑和
vst1q_u32 // 将它们存储回 s
但是我不知道如何以最佳方式做到这一点。例如,我应该在加载、和存储之后将 s,m 增加 4 吗?我对 NEON 很陌生,所以我真的需要一些帮助。
我正在使用 gcc 4.8.1,并且正在使用以下 cmd 进行编译:
arm-linux-gnueabihf-gcc -mthumb -march=armv7-a -mtune=cortex-a9 -mcpu=cortex-a9 -mfloat-abi=hard -mfpu=neon -O3 -fprefetch-loop-数组名称.c -o 名称
提前致谢
【问题讨论】:
我可以为您提供以下建议:使用 -fno-tree-vectorize 关闭自动矢量化。并且远离内在函数,除非你想花更多的时间调试而不是编码。如果您需要 NEON 来满足您的目的,请进行组装。 感谢您的回复。所以你建议在汇编中编写函数比内在函数更有效?我认为内在函数映射到特定的汇编指令,因此它与编写汇编非常相似。内在函数会导致什么样的问题??? 自从 Linaro 接管了 GCC,它比以前更好,因为 Intrinsics 生成的代码简直就是垃圾。现在,在处理简单示例时,您可能会通过内在函数获得不错的性能。然而,当涉及到需要大量寄存器的实际现场使用时,尤其是当它们被置换时,内在函数会做很多模糊的事情,比如在寄存器之间不必要地传输数据。 【参考方案1】:我可能会这样做。我已经包含了 4x 循环展开。预加载缓存总是一个好主意,并且可以将速度再提高 25%。由于没有太多的处理正在进行(它主要花时间加载和存储),最好加载大量寄存器,然后处理它们,因为它为数据实际加载提供了时间。它假设数据是 16 个元素的偶数倍。
void fmask(unsigned int x, unsigned int y, uint32_t *s, uint32_t *m)
unsigned int ixy;
uint32x4_t srcA,srcB,srcC,srcD;
uint32x4_t maskA,maskB,maskC,maskD;
ixy = xsize * ysize;
ixy /= 16; // process 16 at a time
while (ixy--)
__builtin_prefetch(&s[64]); // preload the cache
__builtin_prefetch(&m[64]);
srcA = vld1q_u32(&s[0]);
maskA = vld1q_u32(&m[0]);
srcB = vld1q_u32(&s[4]);
maskB = vld1q_u32(&m[4]);
srcC = vld1q_u32(&s[8]);
maskC = vld1q_u32(&m[8]);
srcD = vld1q_u32(&s[12]);
maskD = vld1q_u32(&m[12]);
srcA = vandq_u32(srcA, maskA);
srcB = vandq_u32(srcB, maskB);
srcC = vandq_u32(srcC, maskC);
srcD = vandq_u32(srcD, maskD);
vst1q_u32(&s[0], srcA);
vst1q_u32(&s[4], srcB);
vst1q_u32(&s[8], srcC);
vst1q_u32(&s[12], srcD);
s += 16;
m += 16;
【讨论】:
感谢您的回答!这非常非常有用。我的代码速度提高了 60% :) 但是 gcc 编译器的行为很奇怪。当我使用 -O2 标志时,fmask(简单)在 0.398 毫秒内执行,而 fmask(带有内在函数)在 0.24 毫秒内执行。这是一个 60% 的加速。但是,当我使用 -Ofast 时,fmask(简单)需要 0.636 毫秒,而 fmask(内部)需要 0.457 毫秒。我可以理解,由于某些编译器原因,简单的 fmask(simple) 需要比 -O2 更多的时间。但是我认为当我使用 intrinisics 时,编译器不会自动矢量化 fmask_intinsics。 另一个评论是,如果我不使用 s 和 m 的预取指令,那么简单 fmask 版本和使用内在函数的版本的性能完全相同! 较新的 ARM CPU 在检测到循环时会进行自动预缓存;对于这些,您不会看到任何性能差异。在编写高性能代码时,如果可能,我总是用汇编语言编写它,因为编译器(尤其是 GCC)可能会输出性能不佳的代码。这个函数写得很好的 asm 代码看起来与内在函数没有太大区别;勇敢地尝试一下。【参考方案2】:我会从最简单的一个开始,并将其作为参考,以便与以后的例程进行比较。
一个好的经验法则是尽快计算所需的东西,而不是在需要的时候。 这意味着指令可能需要 X 个周期才能执行,但结果并不总是立即就绪,因此调度很重要
例如,您的案例的简单调度架构是(伪代码)
nn=n/4 // Assuming n is a multiple of 4
LOADI_S(0) // Load and immediately after increment pointer
LOADI_M(0) // Load and immediately after increment pointer
for( k=1; k<nn;k++)
AND_SM(k-1) // Inner op
LOADI_S(k) // Load and increment after
LOADI_M(k) // Load and increment after
STORE_S(k-1) // Store and increment after
AND_SM(nn-1)
STORE_S(nn-1) // Store. Not needed to increment
从内部循环中省略这些指令,我们实现了内部的操作不依赖于前一个操作的结果。 可以进一步扩展此模式,以利用在等待上一个操作的结果时会浪费的时间。
此外,由于内在函数仍然依赖于优化器,请查看编译器在不同优化选项下的作用。我更喜欢使用内联汇编,这对于小程序来说并不难,并且给你更多的控制权。
【讨论】:
刚刚看到上面的答案,这是一个更优雅的方式相同的原则。 +1以上是关于使用 ARM SIMD 指令优化掩码功能的主要内容,如果未能解决你的问题,请参考以下文章