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

前端之家收集整理的这篇文章主要介绍了c – 如何在块复制期间矢量化范围检查?前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
我有以下功能
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?现在它没有生成矢量化代码.如果没有自动执行此操作,我可以使用哪些功能来执行此操作?

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

解决方法

我不认为你会得到一个自动矢量化的编译器,你可以用英特尔的内在函数手工完成. (错误,我也可以手工做:P).

可能一旦我们手动向量化它,我们就可以看到如何用标量代码手动保存编译器,但我们真的需要打包比较到带有字节元素的0 / 0xFF,并且很难用C语言编写一些东西.编译器将自动矢量化.默认的整数提升意味着大多数C表达式实际上产生32位结果,即使你使用uint8_t,并且经常欺骗编译器解压缩8位到32位元素,在自动因素的基础上花费大量的shuffle吞吐量损失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有符号值.

甚至仍然可以实现(x-min)<的“无符号比较技巧”. (最大 - 最小). (例如,detecting alphabetic ASCII characters).作为奖励,我们可以将范围转换为减法.如果x 代码,即不包括末尾,因此即使redmin="0" redmax="255也会排除红色=" 0或红色="255的像素.但我通过比较另一种方式解决了这个问题(感谢来自@" nejc和@chtz答案的想法).="" @="" chtz使用饱和的add="" sub而不是比较的想法非常酷.如果你安排的东西让饱和意味着在范围内,那么它适用于包容范围.="" (并且您可以通过选择使得所有256个可能输入在范围内的最小值="" 大值来将alpha分量设置为已知值.这使我们可以避免范围转换为有符号转换,因为无符号饱和可用="" 我们可以将sub="" cmp范围检查和饱和技巧结合起来做sub(包裹越界越低)="" subs(如果第一个sub没有换行,则只到零).然后我们不需要在每个组件上使用andnot或或两个单独的检查;我们已经在一个向量中得到0="" 零结果.="" 因此,只需要两次操作就可以为我们检查的整个像素提供32位值.="" iff所有3个rgb组件都在范围内,该元素将具有特定值.="" (因为我们已经安排alpha组件已经给出了已知值).如果3个组件中的任何一个超出范围,它将具有其他一些值.="" 如果你以另一种方式执行此操作,那么饱和度意味着超出范围,那么你在该方向上有一个独占范围,因为你不能选择一个限制,使得没有值达到0或达到255.你总是可以使无论rgb组件的含义如何,alpha组件都可以在那里给出一个已知值.通过选择无像素可匹配的范围,独占范围可让您滥用此功能始终为false.="" (或者如果有第三个条件,除了每个组件的最小="" 大值,那么也许你想要一个覆盖).="" 显而易见的是使用具有32位元素大小(_mm256_cmpeq_epi32="" vpcmpeqd)的打包比较指令来生成0xff或0x00(我们可以应用="" 合到原始rgb像素值中)以进入="" 出范围.="" <="">

// 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

注意,SSE2版本只需要一条MOVDQA指令来复制src;相同的寄存器是每条指令的目的地.

另请注意,您可以使另一个方向饱和:添加然后添加(使用(256-max)和(256-(最小 – 最大)),我认为)在范围内饱和到0xFF.如果您使用固定掩码(例如对于alpha)或可变掩码(对于某些其他条件)使用零屏蔽来基于某些其他条件排除组件,则这对AVX512BW非常有用.对于sub / subs版本的AVX512BW零屏蔽将考虑组件的范围,即使它们不是,这也可能是有用的.

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

我们可以使用从左到右传播的减法中的进位/借位,而不是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

即_mm256_sub_epi32(RGB_inrange,_mm_set1_epi32(1)).我们只需要每个32位元素的高字节来获得我们想要的alpha值,因为我们使用字节混合将其与源RGB值合并.对于AVX512,这避免了VPMOVM2D zmm1,k1指令将比较结果转换回0 / -1的向量,或者(更昂贵)将每个掩码位用3个零交织以将其用于字节混合.

即使对于AVX2,这个sub而不是cmp也有一个小优势:sub_epi32在Skylake上的更多端口上运行(p0 / p1 / p5对比pc0 / pcmpeq的p0 / p1).在所有其他cpu上,向量整数add / sub在与向量整数比较相同的端口上运行. (Agner Fog’s instruction tables).

另外,如果在带有AVX512的cpu上使用-march = native编译_mm256_cmpeq_epi32(),或以其他方式启用AVX512然后编译正常的AVX2内在函数,一些编译器将愚蠢地使用AVX512比较进入掩码,然后扩展回向量而不是向量只使用VEX编码的vpcmpeqd.因此,即使对于_mm256内在函数版本,我们也使用sub而不是cmp,因为我已经花时间弄清楚它并且表明它在正常AVX2的正常编译情况下至少同样有效. (虽然_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,RGB_mask);  // RGB from src,0/FF from new_alpha

    return alpha_replaced;
}
#endif  // __AVX2__

为此函数设置向量args并使用_mm256_load_si256 / _mm256_store_si256遍历数组. (或者如果你不能保证对齐,则为loadu / storeu.)

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

;; MSVC's inner loop from a caller that loops over an array with it:
;; see the Godbolt link
$LL4@:
    vmovdqu ymm3,YMMWORD PTR [rdx+rax*4]
    vpsubb   ymm0,ymm3,ymm7
    vpsubusb ymm1,ymm0,ymm6
    vpsubd   ymm2,ymm1,ymm5
    vpblendvb ymm3,ymm2,ymm4
    vmovdqu YMMWORD PTR [rcx+rax*4],ymm3
    add      eax,8
    cmp      eax,r8d
    jb       SHORT $LL4@

所以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字节向量的速度快两倍.

猜你在找的C&C++相关文章