C++ 如何以更好的性能进行手动代码矢量化,而不是自动矢量化进行边缘检测

C++ 如何以更好的性能进行手动代码矢量化,而不是自动矢量化进行边缘检测,c++,optimization,avx512,C++,Optimization,Avx512,我一直在学习coursera课程,在某一点上给出了下面的代码,并且讲师声称,由于引导向量化很困难,向量化是通过将#pragma omp simd包含在循环的内部和外部之间来完成的。我如何能够自己对课程中使用的代码进行矢量化,有没有比简单地添加#pragma omp simd并继续学习更好的方法 template<typename P> void ApplyStencil(ImageClass<P> & img_in, ImageClass<P> &am

我一直在学习coursera课程,在某一点上给出了下面的代码,并且讲师声称,由于引导向量化很困难,向量化是通过将
#pragma omp simd
包含在
循环的内部和外部
之间来完成的。我如何能够自己对课程中使用的代码进行矢量化,有没有比简单地添加
#pragma omp simd
并继续学习更好的方法

template<typename P>
void ApplyStencil(ImageClass<P> & img_in, ImageClass<P> & img_out) {

  const int width  = img_in.width;
  const int height = img_in.height;

  P * in  = img_in.pixel;
  P * out = img_out.pixel;

  for (int i = 1; i < height-1; i++)
    for (int j = 1; j < width-1; j++) {
      P val = -in[(i-1)*width + j-1] -   in[(i-1)*width + j] - in[(i-1)*width + j+1] 
    -in[(i  )*width + j-1] + 8*in[(i  )*width + j] - in[(i  )*width + j+1] 
    -in[(i+1)*width + j-1] -   in[(i+1)*width + j] - in[(i+1)*width + j+1];

      val = (val < 0   ? 0   : val);
      val = (val > 255 ? 255 : val);

      out[i*width + j] = val;
    }

}

template void ApplyStencil<float>(ImageClass<float> & img_in, ImageClass<float> & img_out);

这里是一些未经测试的概念验证实现,每个数据包使用4个add、1个fmsub和3个load(而不是9个load、7个add、1个fmsub用于直接实现)。我省略了钳制(至少对于
float
图像,钳制看起来很不寻常,对于
uint8
它什么也不做,除非您将
P val=…
更改为
auto val=…
,正如Peter在评论中所注意到的那样)——但您可以自己轻松添加它

此实现的思想是将左侧和右侧的像素(
x0_2
)以及所有3个像素(
x012
)相加,然后从3个连续行(
a012+b0_2+c012
)中减去中间像素乘以8。 在每个循环结束时,放下
a012
的内容,并将
bX
移动到
aX
cX
移动到
bX
,以进行下一次迭代

applyStencil
函数仅对16个像素的每列应用第一个函数(从
col=1开始,最后仅对最后16列执行可能重叠的计算)。如果输入图像的列数少于18列,则需要进行不同的处理(可能是通过屏蔽加载/存储)

#包括
无效applyStencilColumn(浮点常量*输入,浮点*输出,大小\宽度,大小\高度)
{
如果(高度<3)返回;//检查是否正常
浮动常数*最后一英寸=英寸+高度*宽度;
__m512 a012、b012、b0_2、b1;
__m512常量八=_mm512_set1_ps(8.0);
{
//初始化第一行:
__m512 a0=_mm512_loadu_ps(in-1);
__m512 a1=_mm512_loadu_ps(in+0);
__m512 a2=_mm512_loadu_ps(in+1);
a012=_mm512_add_ps(_mm512_add_ps(a0,a2),a1);
in+=宽度;
__m512 b0=_mm512_loadu_ps(in-1);
b1=_mm512_loadu_ps(in+0);
__m512 b2=_mm512_loadu_ps(in+1);
b0_2=_mm512_add_ps(b0,b2);
b012=_mm512_add_ps(b0_2,b1);
in+=宽度;
}
//跳过输出的第一行:
out+=宽度;

对于(;对于整数类型,我认为还有更大的优化空间。编译器往往不擅长扩大整数,然后再缩小它们。但我认为
uint8
的函数会有缺陷;实际上,你需要
auto P=…
,因为
+
-
在窄整数
P
上会有缺陷在夹紧之前升级到
int val
。但是即使使用
float
,是的,您也可以通过无序排列来排列所涉及的3行中的每一行的正确数据,这样您就可以混合执行加载和无序排列。IDK如果重用部分和的空间很大。无论如何,您的问题标题非常通用,就像您没有一样我想有一些一般性的答案。但是你的问题的答案将是特定于对这个模具问题进行矢量化。这就是为什么手动操作而不是将其留给编译器的基本原因:你必须确切地知道CPU可以/不能有效地做什么,并且自己使用
_mm512_loadu_ps(&n[(i-1)*width+j-1])
。以及在数据访问中发现模式。通过将符号位放入掩码寄存器,然后执行零掩码
vminps
(aka),可以实现最终钳位。这应该比单独的
minps
maxps
(在AVX512中新增)很有趣,但我认为这不是我们需要的;它不能在一条指令中钳制上限和下限。在什么实际硬件上?IceLake笔记本电脑(
-march=skylake client
)、Xeon Phi(
-march=knl
)或Xeon可伸缩skylake SP或Cascade Lake(
skylake-avx512
)?这种差异可能会影响哪种选择最适合洗牌和更多未对齐的64字节加载。我不确定跨列是否能很好地工作。HW预取可以处理固定的步幅,但由于-1和+1偏移,在连续过程中不可避免地多次接触同一缓存线,因此并非所有加载都可以对齐ed.并行执行更多列可能会有所帮助。如果一列缓存线设法适应L2或L1d,则可能会造成伤害,例如,行跨距不是2的大幂,这使得它们都为同一组别名。我认为,在跨列移动时并行执行多行可以让您获得一些重用。但如果对于L2或L1d关联性,您有太多的输入流,并且出现了一个不幸的宽度=行跨距。(不过,在使用AVX512的英特尔CPU上,输入流最少为8路。与只有4路L2关联性的Skylake客户端不同。)浮点数箝位出现的原因是我假设饱和到uin8_t像素值范围,以便在浮点数域中进行可能的进一步计算。如果下一步是转换回uint8_t,那么是的,仅转换为int32_t并使用带无符号饱和的压缩将更有意义。@PeterCords我添加了关于迭代的另一个改进在多行上。我还考虑将
im[-1]+im[0]+im[1]
部分和存储在某个位置(例如,在目标阵列的下一行中,然后添加3行并从中心像素的9倍中减去)。不过,我不会对此进行进一步的研究。关于钳制:“浮动”图像“通常”存储的范围为0到1.0,因此在此处使用255进行钳制不会产生任何效果。但是,钳制到0实际上隐藏了黑色li
❯ gcc -march=native -Q --help=target|grep march
  -march=                           skylake

❯ gcc -march=knl -dM -E - < /dev/null | egrep "SSE|AVX" | sort
#define __AVX__ 1
#define __AVX2__ 1
#define __AVX512CD__ 1
#define __AVX512ER__ 1
#define __AVX512F__ 1
#define __AVX512PF__ 1
#define __SSE__ 1
#define __SSE2__ 1
#define __SSE2_MATH__ 1
#define __SSE3__ 1
#define __SSE4_1__ 1
#define __SSE4_2__ 1
#define __SSE_MATH__ 1
#define __SSSE3__ 1
#include <immintrin.h>

void applyStencilColumn(float const *in, float *out, size_t width, size_t height)
{
  if(height < 3) return; // sanity check
  float const* last_in = in + height*width;
  __m512 a012, b012, b0_2, b1;
  __m512 const eight = _mm512_set1_ps(8.0);
  {
    // initialize first rows:
    __m512 a0 = _mm512_loadu_ps(in-1);
    __m512 a1 = _mm512_loadu_ps(in+0);
    __m512 a2 = _mm512_loadu_ps(in+1);
    a012 = _mm512_add_ps(_mm512_add_ps(a0,a2),a1);
    in += width;
    __m512 b0 = _mm512_loadu_ps(in-1);
    b1 = _mm512_loadu_ps(in+0);
    __m512 b2 = _mm512_loadu_ps(in+1);
    b0_2 = _mm512_add_ps(b0,b2);
    b012 = _mm512_add_ps(b0_2,b1);
    in += width;
  }
  // skip first row for output:
  out += width;

  for(; in<last_in; in+=width, out+=width)
  {
    // precalculate sums for next row:
    __m512 c0 = _mm512_loadu_ps(in-1);
    __m512 c1 = _mm512_loadu_ps(in+0);
    __m512 c2 = _mm512_loadu_ps(in+1);
    __m512 c0_2 = _mm512_add_ps(c0,c2);
    __m512 c012 = _mm512_add_ps(c0_2, c1);

    __m512 outer = _mm512_add_ps(_mm512_add_ps(a012,b0_2), c012);
    __m512 result = _mm512_fmsub_ps(eight, b1, outer);

    _mm512_storeu_ps(out, result);
    // shift/rename registers (with some unrolling this can be avoided entirely)
    a012 = b012;
    b0_2 = c0_2; b012 = c012; b1 = c1;
  }
}

void applyStencil(float const *in, float *out, size_t width, size_t height)
{
  if(width < 18) return; // assert("special case of narrow image not implemented");

  for(size_t col = 1; col < width - 18; col += 16)
  {
    applyStencilColumn(in + col, out + col, width, height);
  }
  applyStencilColumn(in + width - 18, out + width - 18, width, height);
}