将8个字符从内存加载到__m256变量中作为压缩单精度浮点数

问题描述 投票:5回答:1

我正在优化图像上的高斯模糊算法,我想用下面的代码替换__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])];
c++ sse simd avx avx2
1个回答
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位广播负载为高64位提供vpmovzxbd ymm,xmmvpshufb ymm_mm256_shuffle_epi8),因为Intel SnB系列CPU没有微型融合一个vpmovzx ymm,mem,只有vpmovzx xmm,mem。 (https://agner.org/optimize/)。广播负载是单个uop,不需要ALU端口,纯粹在加载端口运行。所以这是bcast-load + vpmovzx + vpshufb的3个uop。

(TODO:写一个内在版本。它还回避了_mm_loadl_epi64错过优化的问题 - > _mm256_cvtepu8_epi32。)

当然,这需要在另一个寄存器中使用shuffle控制向量,所以如果你可以多次使用它,它是值得的。

vpshufb是可用的,因为每个通道所需的数据来自广播,而shuffle-control的高位将使相应的元素归零。

这种广播+随机播放策略可能对Ryzen有好处; Agner Fog没有列出vpmovsx/zx ymm的uop计数。


不要执行类似128位或256位负载的操作,然后随机播放以提供更多vpmovzx指令。总的洗牌吞吐量可能已经成为瓶颈,因为vpmovzx是一个洗牌。英特尔Haswell / Skylake(最常见的AVX2搜索)具有每时钟1次的随机播放,但每次负载为2次。使用额外的shuffle指令而不是将单独的内存操作数折叠到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

你当然永远不需要一个浮点数组,只需要__m256向量。


GCC / MSVC错过了使用内在函数对VPMOVZXBD ymm,[mem]的优化

GCC和MSVC不善于将_mm_loadl_epi64折叠成vpmovzx*的内存操作数。 (但至少有一个正确宽度的载荷固有,不像pmovzxbq xmm, word [mem]。)

我们得到一个vmovq负载,然后一个单独的vpmovzx与XMM输入。 (使用ICC和clang3.6 +我们从使用_mm_loadl_epi64获得安全+最佳代码,例如来自gcc9 +)

但是gcc8.3和更早版本可以将_mm_loadu_si128 16字节加载内在函数折叠成8字节的内存操作数。这在GCC上的-O3上给出了最佳asm,但在-O0处是不安全的,在那里它编译为实际的vmovdqu负载,触及我们实际加载的更多数据,并且可能在页面的末尾。

由于这个答案提交了两个gcc错误:


使用SSE4.1 pmovsx / pmovzx作为负载是没有内在的,只有__m128i源操作数。但asm指令只读取它们实际使用的数据量,而不是16字节的__m128i内存源操作数。与punpck*不同,您可以在页面的最后一个8B上使用它而不会出现错误。 (即使使用非AVX版本,也可以使用未对齐的地址)。

所以这是我提出的邪恶解决方案。不要使用它,#ifdef __OPTIMIZE__是坏的,这样就可以创建只在调试版本中发生或仅在优化版本中发生的错误!

#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有两个单独的条目:

  • movd / movq,可以将整数寄存器作为src / dest操作数的版本(66 REX.W 0F 6E(或VEX.128.66.0F.W1 6E),用于(V)MOVQ xmm,r / m64)。在那里你可以找到可以接受64位整数_mm_cvtsi64_si128的内在函数。 (有些编译器没有在32位模式下定义它。)
  • movq:可以有两个xmm寄存器作为操作数的版本。这是MMXreg - > MMXreg指令的扩展,它也可以像MOVDQU一样加载/存储。它的操作码F3 0F 7EVEX.128.F3.0F.WIG 7E)为MOVQ xmm, xmm/m64)。 asm ISA ref手册仅列出了m128i _mm_mov_epi64(__m128i a)内在函数,用于在复制时将向量的高64b归零。但the intrinsics guide does list _mm_loadl_epi64(__m128i const* mem_addr)有一个愚蠢的原型(指向一个16字节的__m128i类型,当它真的只加载8个字节)。它可以在所有4个主要x86编译器上使用,实际上应该是安全的。请注意,__m128i*只是传递给这个不透明的内在函数,而不是实际解除引用。 更健全的_mm_loadu_si64 (void const* mem_addr)也被列出,但是gcc缺少那一个。
© www.soinside.com 2019 - 2024. All rights reserved.