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?现在它没有生成矢量化代码.如果没有自动执行此操作,我可以使用哪些功能来执行此操作?
因为实际上,我想象一下测试字节是否在一个范围内将是最明显有用的操作之一,但我看不到任何内置函数来处理它.
解决方法
可能一旦我们手动向量化它,我们就可以看到如何用标量代码手动保存编译器,但我们真的需要打包比较到带有字节元素的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
// 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字节向量的速度快两倍.