我正在优化图像上的高斯模糊算法,并且想用下面的__m256内在变量替换浮点缓冲区[8]的用法。什么系列的说明最适合此任务?
// unsigned char *new_image is loaded with data
...
float buffer[8];
buffer[x ] = new_image[x];
buffer[x + 1] = new_image[x + 1];
buffer[x + 2] = new_image[x + 2];
buffer[x + 3] = new_image[x + 3];
buffer[x + 4] = new_image[x + 4];
buffer[x + 5] = new_image[x + 5];
buffer[x + 6] = new_image[x + 6];
buffer[x + 7] = new_image[x + 7];
// buffer is then used for further operations
...
//What I want instead in pseudocode:
__m256 b = [float(new_image[x+7]), float(new_image[x+6]), ... , float(new_image[x])];
最佳答案
如果您使用的是AVX2,则可以使用PMOVZX将char零扩展为256b寄存器中的32位整数。从那里开始,转换为float可以就地进行。
; rsi = new_image
VPMOVZXBD ymm0, [rsi] ; or SX to sign-extend (Byte to DWord)
VCVTDQ2PS ymm0, ymm0 ; convert to packed foat
即使您要对多个 vector 执行此操作,这也是一个很好的策略,但更好的方法可能是一个128位的广播负载,以为高64位的
vpmovzxbd ymm,xmm
和vpshufb ymm
(_mm256_shuffle_epi8
)提供,因为Intel SnB系列CPU不支持不会微融合vpmovzx ymm,mem
,而只能微融合vpmovzx xmm,mem
。 (https://agner.org/optimize/)。广播负载是单个uop,不需要ALU端口,仅在负载端口中运行。因此,这是bcast-load + vpmovzx + vpshufb的3个总计。(TODO:编写该函数的内在版本。它也回避了
_mm_loadl_epi64
-> _mm256_cvtepu8_epi32
缺少优化的问题。)当然,这需要另一个寄存器中的混洗控制 vector ,因此只有多次使用它才值得。
vpshufb
可用,因为从广播中可以找到每个 channel 所需的数据,并且随机控制的高位会将相应的元素清零。这种广播+随机播放策略在Ryzen上可能不错。 Agner Fog没有在上面列出
vpmovsx/zx ymm
的uop计数。不要而不是做类似128位或256位加载的操作,然后将其改组以提供进一步的
vpmovzx
指令。总洗牌吞吐量可能已经成为瓶颈,因为vpmovzx
是一个洗牌。英特尔Haswell/Skylake(最常见的AVX2架构)具有每时钟1个随机播放,但每时钟2个负载。使用额外的随机播放指令而不是将单独的内存操作数折叠为vpmovzxbd
是很糟糕的。只有像我建议的那样,通过广播负载+ vpmovzxbd + vpshufb减少总uop计数,它才是胜利。我对Scaling byte pixel values (y=ax+b) with SSE2 (as floats)?的回答可能与转换回
uint8_t
有关。如果使用AVX2 packssdw/packuswb
进行打包,那么后面打包的字节将是半精打细算的,因为与vpmovzx
不同,它们在车道内工作。仅使用AVX1,而不使用AVX2 ,您应该执行以下操作:
VPMOVZXBD xmm0, [rsi]
VPMOVZXBD xmm1, [rsi+4]
VINSERTF128 ymm0, ymm0, xmm1, 1 ; put the 2nd load of data into the high128 of ymm0
VCVTDQ2PS ymm0, ymm0 ; convert to packed float. Yes, works without AVX2
当然,您永远不需要一个float数组,只需
__m256
vector 。GCC/MSVC缺少使用内在函数的
VPMOVZXBD ymm,[mem]
优化GCC和MSVC很难将
_mm_loadl_epi64
折叠到vpmovzx*
的内存操作数中。 (但与pmovzxbq xmm, word [mem]
不同,至少有一个正确宽度的固有负载)。我们得到一个
vmovq
加载,然后得到一个带有XMM输入的单独的vpmovzx
。 (使用ICC和clang3.6 +,我们可以通过使用_mm_loadl_epi64
获得安全+最佳代码,例如gcc9 +)但是gcc8.3和更早版本可以将固有的
_mm_loadu_si128
16字节负载折叠为8字节内存操作数。这会在GCC上的-O3
处提供最佳的asm,但在-O0
处会编译为实际的vmovdqu
加载,这会涉及到我们实际加载的更多数据,因此可能不安全,这可能会超出页面的末尾。由于此答案,提交了两个gcc错误:
MOVQ m64, %xmm
in 32bit mode。 (TODO:也为clang/LLVM报告此信息?)没有将SSE4.1
pmovsx
/pmovzx
用作负载的内在函数,仅使用__m128i
源操作数。但是,asm指令仅读取它们实际使用的数据量,而不读取16字节的__m128i
内存源操作数。与punpck*
不同,您可以在页面的最后8B使用它,而不会出错。 (甚至在非对齐地址上,即使使用非AVX版本也是如此)。所以这是我想出的邪恶解决方案。不要使用它,
#ifdef __OPTIMIZE__
是Bad,可以创建仅在调试版本中或仅在优化版本中发生的错误!#if !defined(__OPTIMIZE__)
// Making your code compile differently with/without optimization is a TERRIBLE idea
// great way to create Heisenbugs that disappear when you try to debug them.
// Even if you *plan* to always use -Og for debugging, instead of -O0, this is still evil
#define USE_MOVQ
#endif
__m256 load_bytes_to_m256(uint8_t *p)
{
#ifdef USE_MOVQ // compiles to an actual movq then movzx ymm, xmm with gcc8.3 -O3
__m128i small_load = _mm_loadl_epi64( (const __m128i*)p);
#else // USE_LOADU // compiles to a 128b load with gcc -O0, potentially segfaulting
__m128i small_load = _mm_loadu_si128( (const __m128i*)p );
#endif
__m256i intvec = _mm256_cvtepu8_epi32( small_load );
//__m256i intvec = _mm256_cvtepu8_epi32( *(__m128i*)p ); // compiles to an aligned load with -O0
return _mm256_cvtepi32_ps(intvec);
}
在启用USE_MOVQ的情况下,
gcc -O3
(v5.3.0) emits。 (MSVC也是如此)load_bytes_to_m256(unsigned char*):
vmovq xmm0, QWORD PTR [rdi]
vpmovzxbd ymm0, xmm0
vcvtdq2ps ymm0, ymm0
ret
我们要避免愚蠢的
vmovq
。如果让它使用不安全的loadu_si128
版本,它将成为良好的优化代码。GCC9,clang和ICC发出:
load_bytes_to_m256(unsigned char*):
vpmovzxbd ymm0, qword ptr [rdi] # ymm0 = mem[0],zero,zero,zero,mem[1],zero,zero,zero,mem[2],zero,zero,zero,mem[3],zero,zero,zero,mem[4],zero,zero,zero,mem[5],zero,zero,zero,mem[6],zero,zero,zero,mem[7],zero,zero,zero
vcvtdq2ps ymm0, ymm0
ret
使用内在函数编写仅AVX1版本的内容对于读者来说是一项无聊的练习。您要求的是“指令”,而不是“内在的”,而这是内在函数存在差异的地方。 IMO不得不使用
_mm_cvtsi64_si128
以避免可能从越界地址加载。我希望能够根据它们映射到的指令来考虑内在函数,而将加载/存储内在函数告知编译器关于对齐保证或缺少对齐保证。在我不想要的指令中使用内在函数是相当愚蠢的。另请注意,如果您要查找《英特尔insn引用手册》,则movq有两个单独的条目:
66 REX.W 0F 6E
(或VEX.128.66.0F.W1 6E
)。在这里,您将找到可以接受64位整数_mm_cvtsi64_si128
的内在函数。 (某些编译器未在32位模式下对其进行定义。)F3 0F 7E
(VEX.128.F3.0F.WIG 7E
)用于MOVQ xmm, xmm/m64)
。asm ISA ref手册仅列出了在复制 vector 时将其高64b调零的
m128i _mm_mov_epi64(__m128i a)
内在函数。但是the intrinsics guide does list _mm_loadl_epi64(__m128i const* mem_addr)
有一个愚蠢的原型(prototype)(当它实际上只加载8个字节时,指向16字节__m128i
类型的指针)。它在所有4种主要x86编译器上都可用,并且实际上应该是安全的。请注意,__m128i*
只是传递给此不透明的内部函数,实际上并未取消引用。更加合理的
_mm_loadu_si64 (void const* mem_addr)
也被列出,但是gcc缺少那个。