8

我正在优化图像上的高斯模糊算法,我想用 __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])];
4

1 回答 1

11

如果您使用的是 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,xmmvpshufb ymm( _mm256_shuffle_epi8) 用于高 64 位,因为英特尔 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。)

当然,这需要另一个寄存器中的随机控制向量,所以只有你可以多次使用它才值得。

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

这种广播+洗牌的策略可能对锐龙有好处;Agner Fog 没有列出 uop 计数vpmovsx/zx ymm


不要执行 128 位或 256 位加载之类的操作,然后其洗牌以提供进一步的vpmovzx指令。shuffle 的总吞吐量可能已经是一个瓶颈,因为vpmovzx它是一个 shuffle。Intel Haswell/Skylake(最常见的 AVX2 uarches)每时钟 1 次随机播放,但每时钟 2 次加载。使用额外的 shuffle 指令而不是将单独的内存操作数折叠起来vpmovzxbd是很糟糕的。只有当您可以像我建议的广播负载 + vpmovzxbd + vpshufb 那样减少总 uop 计数时,它才是胜利。


我对使用 SSE2(作为浮点数)缩放字节像素值(y=ax+b)的回答?可能与转换回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 不擅长将 a 折叠_mm_loadl_epi64vpmovzx*. (但至少有一个正确宽度的负载内在,不像 for pmovzxbq xmm, word [mem]。)

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

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

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


pmovsx使用 SSE4.1 /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) 会发出. (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 的版本对于读者来说是一个不有趣的练习。您要求的是“指令”,而不是“内在函数”,这是内在函数存在差距的一个地方。必须使用_mm_cvtsi64_si128以避免可能从越界地址加载是愚蠢的,IMO。我希望能够根据它们映射到的指令来考虑内在函数,并将加载/存储内在函数告知编译器对齐保证或缺乏对齐保证。必须将内在函数用于我不想要的指令是非常愚蠢的。


另请注意,如果您正在查看英特尔 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 7E( VEX.128.F3.0F.WIG 7E) 为MOVQ xmm, xmm/m64).

    asm ISA 参考手册仅列出了m128i _mm_mov_epi64(__m128i a)在复制向量时将向量的高 64b 归零的内在函数。但是内在函数指南确实列出_mm_loadl_epi64(__m128i const* mem_addr)了具有愚蠢原型的列表(__m128i当它实际上只加载 8 个字节时,它是指向 16 字节类型的指针)。它在所有 4 个主要的 x86 编译器上都可用,并且实际上应该是安全的。请注意,__m128i*只是传递给这个不透明的内在函数,而不是实际取消引用。

    _mm_loadu_si64 (void const* mem_addr)还列出了更理智的,但 gcc 缺少那个。

于 2015-12-15T03:10:32.750 回答