如何在块复制期间矢量化范围检查?

时间:2018-03-27 15:25:58

标签: c++ vectorization sse avx

我有以下功能:

void CopyImageBitsWithAlphaRGBA(unsigned char *dest, const unsigned char *src, int w, int stride, int h,
    unsigned char minredmask, unsigned char mingreenmask, unsigned char minbluemask, unsigned char maxredmask, unsigned char maxgreenmask, unsigned char maxbluemask)
{
    auto pend = src + w * h * 4;
    for (auto p = src; p < pend; p += 4, dest += 4)
    {
        dest[0] = p[0]; dest[1] = p[1]; dest[2] = p[2];
        if ((p[0] >= minredmask && p[0] <= maxredmask) || (p[1] >= mingreenmask && p[1] <= maxgreenmask) || (p[2] >= minbluemask && p[2] <= maxbluemask))
            dest[3] = 255;
        else
            dest[3] = 0;
    }
}

它的作用是将32位位图从一个存储块复制到另一个存储块,当像素颜色落在某个颜色范围内时,将alpha通道设置为完全透明。

如何在VC ++ 2017中使用SSE / AVX?现在它还没有生成矢量化代码。如果没有自动执行此操作,我可以使用哪些功能来自行完成此操作?

因为实际上,我想象一下测试字节是否在一个范围内将是最明显有用的操作之一,但是我无法看到任何内置函数来处理它。

3 个答案:

答案 0 :(得分:6)

我认为您不会让编译器自动进行矢量化,而且您可以手动使用英特尔的内在函数。 (错误,以及 I 无论如何都可以手工完成:P)。

可能一旦我们手动对其进行矢量化,我们就可以看到如何用一种以这种方式工作的标量代码来手持编译器,但是我们真的需要打包 - 比较到带有字节元素的0 / 0xFF,并且它是&#39; s很难在C中写一些编译器可以自动矢量化的东西。默认的整数提升意味着大多数C表达式实际上产生32位结果,即使你使用uint8_t,并且通常会欺骗编译器解压缩8位到32位元素,这会导致大量的混乱。 4吞吐量损失的自动因子(每个寄存器的元素更少),like in @harold's small tweak to your source

SSE / AVX(在AVX512之前)对SIMD整数进行签名比较,而非无符号。但你可以通过减去128来将范围转换为带符号-128..127。在某些CPU上,XOR(无加载)稍微提高效率,所以你实际上只需用0x80进行异或来翻转高位。但是在数学上你要从0..255无符号值中减去128,得到-128..127有符号值。

甚至还可以实现&#34;无符号比较技巧&#34; (x-min) < (max-min)。 (例如,detecting alphabetic ASCII characters)。作为奖励,我们可以将范围转换为减法。如果x<min,它会回绕并变为大于max-min的大值。这显然适用于无符号,但事实上它(使用范围转移max-min)与SSE / AVX2签名比较指令一起工作。 (此答案的先前版本声称此技巧仅在max-min < 128时才有效,但事实并非如此。x-min无法完全包裹并且低于{{max-min 1}},或者如果它开始于max以上,则进入该范围。

此答案的早期版本的代码使得范围独占,即不包括结尾,因此即使redmin = 0 / redmax = 255也会排除红色= 0或红色=的像素255。但是我通过比较另一种方式解决了这个问题(感谢来自@ Nejc&@ 39和@ chtz&#39的答案)。

@ chtz使用饱和的add / sub 代替进行比较的想法非常酷。如果你安排的东西让饱和意味着在范围内,那么它适用于包容范围。 (并且您可以通过选择使得所有256个可能输入在范围内的最小值/最大值来将Alpha分量设置为已知值。 这使我们可以避免范围转换为有符号转换,因为无符号饱和可用

我们可以将sub / cmp范围检查和饱和技巧结合起来sub(包裹越界越低)/ subs(如果第一个{{1}则只到零没有包装)。然后,我们不需要subandnot来对每个组件组合两个单独的检查;我们在一个向量中已经有or /非零结果。

因此,只需要两次操作就可以为我们检查的整个像素提供32位值。 Iff所有3个RGB组件都在范围内,该元素将具有特定值。 (因为我们已经安排Alpha组件已经给出了已知值)。如果3个组件中的任何一个超出范围,它将具有其他一些值。

如果以另一种方式执行此操作,那么饱和度意味着超出范围,那么您在该方向上具有独占范围,因为您无法选择限制,使得没有值达到0或达到255。无论RGB组件的意味着什么,您都可以随时使alpha组件饱和,为自己提供一个已知值。通过选择无像素可匹配的范围,独占范围可让您滥用此功能始终为false。 (或者如果还有第三个条件,除了每个组件的最小/最大值,那么也许你想要一个覆盖)。

显而易见的是使用32位元素大小的打包比较指令0 / _mm256_cmpeq_epi32生成{{1或vpcmpeqd (我们可以将其应用/混合到原始RGB像素值中)进入/超出范围。

0xFF

请注意,SSE2版本只需要一条MOVDQA指令即可复制0x00;相同的寄存器是每条指令的目的地。

另请注意,您可以使另一个方向饱和:// AVX2 core idea: wrapping-compare trick with saturation to achieve unsigned compare __m256i tmp = _mm256_sub_epi8(src, min_values); // wraps to high unsigned if below min __m256i RGB_inrange = _mm256_subs_epu8(tmp, max_minus_min); // unsigned saturation to 0 means in-range __m256i new_alpha = _mm256_cmpeq_epi32(RGB_inrange, _mm256_setzero_si256()); // then blend the high byte of each element with RGB from the src vector __m256i alpha_replaced = _mm256_blendv_epi8(new_alpha, src, _mm256_set1_epi32(0x00FFFFFF)); // alpha from new_alpha, RGB from src 然后src(我认为addadds)在范围内饱和到0xFF。这对于AVX512BW 如果你使用固定掩码的零屏蔽(例如对于alpha)或可变掩码(对于某些其他条件)来排除基于的组件可能很有用一些其他条件。对于sub / subs版本的AVX512BW零屏蔽将考虑组件在范围内,即使它们不是,这也可能是有用的。

但是将其扩展到AVX512需要采用不同的方法:AVX512比较产生位掩码(在掩码寄存器中),而不是矢量,因此我们无法转身并使用每个32位比较结果的高字节分别。

我们可以使用从左到右传播的减法中的进位/借位,而不是(256-max),而不是(256-(min-max)),我们可以在每个像素的高字节中产生我们想要的值。

cmpeq_epi32

即。 0x00000000 - 1 = 0xFFFFFFFF # high byte = 0xFF = new alpha 0x00?????? - 1 = 0x00?????? # high byte = 0x00 = new alpha Where ?????? has at least one non-zero bit, so it's a 32-bit number >=0 and <=0x00FFFFFFFF Remember we choose an alpha range that makes the high byte always zero 。我们只需要每个32位元素的高字节来获得我们想要的alpha值,因为我们使用字节混合将其与源RGB值合并。对于AVX512,这避免了_mm256_sub_epi32(RGB_inrange, _mm_set1_epi32(1))指令将比较结果转换回0 / -1的向量,或者(更昂贵)将每个掩码位用3个零交织以将其用于字节混合。

即使对于AVX2 ,此VPMOVM2D zmm1, k1代替sub也有一个小优势:cmp在Skylake上的更多端口上运行(p0 / p1 / p5 vs. p0 / p1为pcmpgt / pcmpeq)。在所有其他CPU上,向量整数add / sub在与向量整数比较相同的端口上运行。 (Agner Fog's instruction tables)。

此外,如果您在带有AVX512的CPU上使用sub_epi32编译_mm256_cmpeq_epi32(),或以其他方式启用AVX512然后编译正常的AVX2内在函数,一些编译器将愚蠢地使用AVX512比较进入掩码然后展开回到矢量而不是仅使用VEX编码的-march=native。因此,即使对于vpcmpeqd内在函数版本,我们也使用sub代替cmp,因为我已经花了很多时间来弄清楚它并且表明它至少在效率方面有效编译常规AVX2的正常情况。 (虽然_mm256_mm256_setzero_si256()便宜; set1(1)可以廉价地将寄存器归零,而不是加载常量,但此设置在循环外发生。)

vpxor

为此函数设置矢量参数,并使用#include <immintrin.h> #ifdef __AVX2__ // inclusive min and max __m256i setAlphaFromRangeCheck_AVX2(__m256i src, __m256i mins, __m256i max_minus_min) { __m256i tmp = _mm256_sub_epi8(src, mins); // out-of-range wraps to a high signed value // (x-min) <= (max-min) equivalent to: // (x-min) - (max-min) saturates to zero __m256i RGB_inrange = _mm256_subs_epu8(tmp, max_minus_min); // 0x00000000 for in-range pixels, 0x00?????? (some higher value) otherwise // this has minor advantages over compare against zero, see full comments on Godbolt __m256i new_alpha = _mm256_sub_epi32(RGB_inrange, _mm256_set1_epi32(1)); // 0x00000000 - 1 = 0xFFFFFFFF // 0x00?????? - 1 = 0x00?????? high byte = new alpha value const __m256i RGB_mask = _mm256_set1_epi32(0x00FFFFFF); // blend mask // without AVX512, the only byte-granularity blend is a 2-uop variable-blend with a control register // On Ryzen, it's only 1c latency, so probably 1 uop that can only run on one port. (1c throughput). // For 256-bit, that's 2 uops of course. __m256i alpha_replaced = _mm256_blendv_epi8(new_alpha, src, RGB_mask); // RGB from src, 0/FF from new_alpha return alpha_replaced; } #endif // __AVX2__ / _mm256_load_si256遍历数组。 (或者如果你不能保证对齐,请加载/存储。)

compiles very efficiently (Godbolt Compiler explorer)与gcc,clang和MSVC。 (Godbolt上的AVX2版本很好,AVX512和SSE版本仍然很乱,并不是所有的技巧都适用于它们。)

_mm256_store_si256

因此,MSVC在内联后设法提升了常量设置。我们从gcc / clang得到类似的循环。

循环有4个向量ALU指令,其中一个指令占用2个uop。共有5个向量ALU uops。但是Haswell / Skylake上的总融合域uops = 9没有展开,所以幸运的是,每2.25个时钟周期可以运行32个字节(1个向量)。它可能接近实际实现L1d或L2缓存中的数据热,但L3或内存将成为瓶颈。通过展开,它可能是L2缓存带宽的瓶颈。

AVX512版本(也包含在Godbolt链接中),只需要1个uop进行混合,并且每个周期可以在向量中运行得更快,因此使用512字节向量的速度快两倍。

答案 1 :(得分:3)

这是使此功能与SSE指令一起使用的一种可能方法。我使用SSE而不是AVX因为我想保持答案简单。一旦你理解了解决方案的工作原理,用AVX内在函数重写函数应该不是什么大问题。

编辑:请注意我的方法与PeterCordes非常类似,但他的代码应该更快,因为他使用AVX。如果您想使用AVX内在函数重写下面的函数,请将step值更改为8

void CopyImageBitsWithAlphaRGBA(
  unsigned char *dest,
  const unsigned char *src, int w, int stride, int h,
  unsigned char minred, unsigned char mingre, unsigned char minblu,
  unsigned char maxred, unsigned char maxgre, unsigned char maxblu)
{
  char low = 0x80; // -128
  char high = 0x7f; // 127
  char mnr = *(char*)(&minred) - low;
  char mng = *(char*)(&mingre) - low;
  char mnb = *(char*)(&minblu) - low;
  int32_t lowest = mnr | (mng << 8) | (mnb << 16) | (low << 24);

  char mxr = *(char*)(&maxred) - low;
  char mxg = *(char*)(&maxgre) - low;
  char mxb = *(char*)(&maxblu) - low;
  int32_t highest = mxr | (mxg << 8) | (mxb << 16) | (high << 24);

  // SSE
  int step = 4;
  int sse_width = (w / step)*step;

  for (int y = 0; y < h; ++y)
  {
    for (int x = 0; x < w; x += step)
    {
      if (x == sse_width)
      {
        x = w - step;
      }

      int ptr_offset = y * stride + x;
      const unsigned char* src_ptr = src + ptr_offset;
      unsigned char* dst_ptr = dest + ptr_offset;

      __m128i loaded = _mm_loadu_si128((__m128i*)src_ptr);

      // subtract 128 from every 8-bit int
      __m128i subtracted = _mm_sub_epi8(loaded, _mm_set1_epi8(low));

      // greater than top limit? 
      __m128i masks_hi = _mm_cmpgt_epi8(subtracted, _mm_set1_epi32(highest));

     // lower that bottom limit?
     __m128i masks_lo = _mm_cmplt_epi8(subtracted, _mm_set1_epi32(lowest));

     // perform OR operation on both masks
     __m128i combined = _mm_or_si128(masks_hi, masks_lo);

     // are 32-bit integers equal to zero?
     __m128i eqzer = _mm_cmpeq_epi32(combined, _mm_setzero_si128());

     __m128i shifted = _mm_slli_epi32(eqzer, 24);

    // EDIT: fixed a bug:
     __m128 alpha_unmasked = _mm_and_si128(loaded, _mm_set1_epi32(0x00ffffff));

     __m128i combined = _mm_or_si128(alpha_unmasked, shifted);

     _mm_storeu_si128((__m128i*)dst_ptr, combined);
    }
  }
}

编辑:正如@PeterCordes在评论中所述,代码中包含一个现已修复的错误。

答案 2 :(得分:2)

基于@PeterCordes解决方案,但用饱和减法替换shift + compare并添加:

// mins_compl shall be [255-minR, 255-minG, 255-minB, 0]
// maxs       shall be [maxR, maxG, maxB, 0]
__m256i  setAlphaFromRangeCheck(__m256i src, __m256i mins_compl, __m256i maxs)
{
    __m256i in_lo = _mm256_adds_epu8(src, mins_compl); // is 255 iff src+mins_coml>=255, i.e. src>=mins
    __m256i in_hi = _mm256_subs_epu8(src, maxs);       // is 0 iff src - maxs <= 0, i.e., src <= maxs

    __m256i inbounds_components = _mm256_andnot_si256(in_hi, in_lo);
    // per-component mask, 0xff, iff (mins<=src && src<=maxs).
    // alpha-channel is always (~src & src) == 0

    // Use a 32-bit element compare to check that all 3 components are in-range
    __m256i RGB_mask = _mm256_set1_epi32(0x00FFFFFF);
    __m256i inbounds = _mm256_cmpeq_epi32(inbounds_components, RGB_mask);

    __m256i new_alpha = _mm256_slli_epi32(inbounds, 24);
    // alternatively _mm256_andnot_si256(RGB_mask, inbounds) ?

    // byte blends (vpblendvb) are at least 2 uops, and Haswell requires port5
    // instead clear alpha and then OR in the new alpha (0 or 0xFF)
    __m256i alphacleared = _mm256_and_si256(src, RGB_mask);   // off the critical path
    __m256i new_alpha_applied = _mm256_or_si256(alphacleared, new_alpha);

    return new_alpha_applied;
}

这保存在vpxor(不需要修改src)和一个vpand(alpha通道自动为0 - 我想这可能是彼得&#39;通过相应地选择边界来解决问题。)

Godbolt-Link,显然,gcc和clang都认为重新使用RGB_mask这两种用法都是值得的...

使用SSE2变体进行简单测试:https://wandbox.org/permlink/eVzFHljxfTX5HDcq(您可以使用源和边界)