使用 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 指令优化掩码功能的主要内容,如果未能解决你的问题,请参考以下文章

利用ARM NEON intrinsic优化常用数学运算

Android neon加速优化

使用 SIMD 指令的平滑样条曲线

SIMD 优化难题

使用 iPhone 的 SIMD 浮点单元将浮点数转换为整数

ARMv8 SIMD和浮点指令编程Libyuv I420 转 ARGB 流程分析