c++ - 将 8 个字符从内存加载到 __m256 变量中作为打包的单精度 float

标签 c++ sse simd avx avx2

我正在优化图像上的高斯模糊算法,我想用 __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 将字符零扩展为 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 位广播负载 vpmovzxbd ymm,xmmvpshufb ymm ( _mm256_shuffle_epi8 ) 用于高 64 位, 因为 Intel SnB 系列 CPU 不会微熔断 vpmovzx ymm,mem ,只有 vpmovzx xmm,mem . ( https://agner.org/optimize/ )。广播负载是单 uop,不需要 ALU 端口,纯粹在负载端口中运行。所以这是 bcast-load + vpmovzx + vpshufb 总共 3 个 uops。

(待办事项:编写一个内部版本。它还避免了对 _mm_loadl_epi64 -> _mm256_cvtepu8_epi32 错过优化的问题。)

当然,这需要另一个寄存器中的 shuffle 控制 vector ,因此只有您可以多次使用它才值得。
vpshufb是可用的,因为每个 channel 所需的数据都来自广播,而 shuffle-control 的高位将使相应的元素归零。

这种广播 + shuffle 策略可能对 Ryzen 有利; Agner Fog 未列出 vpmovsx/zx ymm 的 uop 计数在上面。

不是 执行类似 128 位或 256 位加载的操作,然后对其进行混洗以进一步提供 vpmovzx指示。总混洗吞吐量可能已经成为瓶颈,因为 vpmovzx是洗牌。 Intel Haswell/Skylake(最常见的 AVX2 uarches)有 1-per-clock shuffle,但有 2-per-clock load。使用额外的 shuffle 指令而不是将单独的内存操作数折叠成 vpmovzxbd太可怕了。只有像我建议的那样使用 broadcast-load + 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 vector 。

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 字节内存操作数中。这给出了 -O3 处的最佳汇编在 GCC 上,但在 -O0 处不安全它编译为实际 vmovdqu load 会接触到我们实际加载的更多数据,并且可能会超出页面的末尾。

由于这个答案提交了两个 gcc 错误:
  • SSE/AVX movq load (_mm_cvtsi64_si128) not being folded into pmovzx ( 已针对 gcc9 修复 ,但此修复会破坏 128 位负载的负载折叠,因此旧 GCC 的变通方法使 gcc9 变得更糟。)
  • No intrinsic for x86 MOVQ m64, %xmm in 32bit mode . (待办事项:也为 clang/LLVM 报告这个?)


  • 没有使用 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-only 版本对于读者来说是一项无趣的练习。您要求的是“说明”,而不是“内在”,这是内在存在差距的地方。不得不使用 _mm_cvtsi64_si128避免潜在地从越界地址加载是愚蠢的,IMO。我希望能够根据它们映射到的指令来考虑内在函数,加载/存储内在函数通知编译器有关对齐保证或缺乏对齐保证。必须将内在指令用于我不想要的指令是非常愚蠢的。

    另请注意,如果您正在查看 Intel insn ref 手册,则 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)用于在复制时将 vector 的高位 64b 置零。但是the intrinsics guide does list _mm_loadl_epi64(__m128i const* mem_addr) 它有一个愚蠢的原型(prototype)(指向 16 字节 __m128i 类型的指针,当它实际上只加载 8 个字节时)。它在所有 4 个主要的 x86 编译器上都可用,并且实际上应该是安全的。请注意 __m128i*只是传递给这个不透明的内在函数,实际上并未取消引用。

    越理智_mm_loadu_si64 (void const* mem_addr)也列出了,但是 gcc 缺少那个。
  • 关于c++ - 将 8 个字符从内存加载到 __m256 变量中作为打包的单精度 float ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34279513/

    相关文章:

    c++ - 在 MSVC 中强制未对齐的位域打包

    c++ - 有没有办法用 googletest 创建自定义参数生成器?

    c - SSE 将寄存器设置为 0.0's and 1.0' s 的最佳方法?

    c++ - 使用 SSE 内在函数将 boolean 数组(8 字节 boolean )转换为 int 或 char

    c - 通过线程和 SIMD 并行化矩阵乘法

    java - 是否可以在Java8中执行SIMD比较指令?

    c++ - C++ 函数内联意味着什么?

    c++ - 捕获全局鼠标滚动,并将自定义鼠标滚动事件发送到应用程序

    delphi - Delphi中的SSE2优化?

    c - 如何将 __m128d simd vector 的内容存储为 double 而不将其作为 union 访问?