我正在优化图像上的高斯模糊算法,我想用下面的代码替换__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])];
答案 0 :(得分:6)
如果您正在使用AVX2,则可以使用PMOVZX将字符零扩展为256b寄存器中的32位整数。从那里,转换为浮动可以就地发生。
; rsi = new_image
VPMOVZXBD ymm0, [rsi] ; or SX to sign-extend (Byte to DWord)
VCVTDQ2PS ymm0, ymm0 ; convert to packed foat
这是一个很好的策略,即使你想为多个矢量执行此操作,但更好的可能是 128位广播负载来提供vpmovzxbd ymm,xmm
和vpshufb ymm
({{1对于高64位,,因为Intel SnB系列CPU不会_mm256_shuffle_epi8
微融合,只有vpmovzx ymm,mem
。 (https://agner.org/optimize/)。广播负载是单个uop,不需要ALU端口,纯粹在加载端口运行。所以这是bcast-load + vpmovzx + vpshufb的3个uop。
(TODO:写一个内在版本。它还回避了vpmovzx xmm,mem
- > _mm_loadl_epi64
错过优化的问题。)
当然这需要在另一个寄存器中使用随机控制向量,因此只有多次使用它才值得。
_mm256_cvtepu8_epi32
是可用的,因为每个通道所需的数据来自广播,而shuffle-control的高位将使相应的元素归零。
这种广播+随机策略可能对Ryzen有好处; Agner Fog没有列出vpshufb
上的uop计数。
不执行类似128位或256位加载的操作,然后将其随机播放以提供更多vpmovsx/zx ymm
指令。总的shuffle吞吐量可能已经成为瓶颈,因为vpmovzx
是一个混乱。英特尔Haswell / Skylake(最常见的AVX2搜索)具有每时钟1次的随机播放,但每次负载为2次。使用额外的shuffle指令而不是将单独的内存操作数折叠到vpmovzx
是非常糟糕的。只有你能减少总的uop数量,就像我建议使用broadcast-load + vpmovzxbd + vpshufb一样,这是一场胜利。
我对Scaling byte pixel values (y=ax+b) with SSE2 (as floats)?的回答可能与转换回vpmovzxbd
有关。如果使用AVX2 uint8_t
执行此操作,则返回到字节后的部分是半技巧的,因为它们在内部工作,与packssdw/packuswb
不同。
只有AVX1,而不是AVX2 ,你应该这样做:
vpmovzx
你当然不需要一个浮点数组,只需要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
个向量。
GCC / MSVC错过了使用内在函数的__m256
优化
GCC和MSVC不能将VPMOVZXBD ymm,[mem]
折叠成_mm_loadl_epi64
的内存操作数。 (但至少是正确宽度的负载内在,与vpmovzx*
不同。)
我们得到pmovzxbq xmm, word [mem]
加载,然后是带有XMM输入的单独vmovq
。 (使用ICC和clang3.6 +我们可以使用vpmovzx
获得安全+最佳代码,例如来自gcc9 +)
但是gcc8.3和更早的可以将_mm_loadl_epi64
个16字节的加载内在函数折叠成8字节的内存操作数。这会在GCC上的_mm_loadu_si128
处给出最佳asm,但在-O3
处不安全,它会编译为实际-O0
负载,触及我们实际加载的更多数据,并且可能会结束一页。
由于这个答案提交了两个gcc错误:
MOVQ m64, %xmm
in 32bit mode。 (TODO:同样报告clang / LLVM?)使用SSE4.1 vmovdqu
/ pmovsx
作为加载没有固有的内容,只能使用pmovzx
源操作数。但asm指令只读取它们实际使用的数据量,而不是16字节__m128i
内存源操作数。与__m128i
不同,您可以在页面的最后一个8B上使用它而不会出现错误。 (即使使用非AVX版本,也可以使用未对齐的地址。)
所以这就是我提出的邪恶解决方案。不要使用它,punpck*
是坏的,这样就可以创建只在调试版本中发生或仅在优化版本中发生的错误!
#ifdef __OPTIMIZE__
启用USE_MOVQ后,gcc -O3
(v5.3.0) emits。 (MSVC也是如此)
#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);
}
愚蠢的load_bytes_to_m256(unsigned char*):
vmovq xmm0, QWORD PTR [rdi]
vpmovzxbd ymm0, xmm0
vcvtdq2ps ymm0, ymm0
ret
是我们想要避免的。如果你让它使用不安全的vmovq
版本,那么它将是一个很好的优化代码。
GCC9,clang和ICC发出:
loadu_si128
使用内在函数编写仅限AVX1的版本对读者来说是一种无趣的练习。您要求"指示"而不是"内在函数",这是内在因素存在差距的地方。 IMO,必须使用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
来避免可能从越界地址加载,这是愚蠢的。我希望能够根据它们映射到的指令来考虑内在函数,加载/存储内在函数通知编译器有关对齐保证或缺少对齐保证。必须使用内在的指令,我不想要的是非常愚蠢。
另请注意,如果您正在查看英特尔insn参考手册,则movq有两个单独的条目:
movd / movq,可以将整数寄存器作为src / dest操作数的版本(_mm_cvtsi64_si128
(或66 REX.W 0F 6E
)(V)MOVQ xmm,r / m64)。您可以在其中找到可接受64位整数VEX.128.66.0F.W1 6E
的内在函数。 (有些编译器不会以32位模式定义它。)
movq:可以有两个xmm寄存器作为操作数的版本。这是MMXreg的扩展 - > MMXreg指令,也可以像MOVDQU一样加载/存储。其_mm_cvtsi64_si128
的操作码F3 0F 7E
(VEX.128.F3.0F.WIG 7E
)。
asm ISA ref手册仅列出MOVQ xmm, xmm/m64)
内在函数,用于在复制时将向量的高64b归零。但the intrinsics guide does list _mm_loadl_epi64(__m128i const* mem_addr)
有一个愚蠢的原型(当它真正只加载8个字节时,指向一个16字节m128i _mm_mov_epi64(__m128i a)
类型的指针)。它可以在所有4个主要x86编译器上使用,实际上应该是安全的。请注意,__m128i
只是传递给这个不透明的内在函数,而不是实际上已取消引用。
还列出了更合理的__m128i*
,但是gcc缺少那个。