将内存中的 8 个字符作为打包的单精度浮点数加载到 __m256 变量中
Loading 8 chars from memory into an __m256 variable as packed single precision floats
我正在优化图像上的高斯模糊算法,我想用 __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 位整数。从那里,可以就地转换为浮点数。
; 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
(_mm256_shuffle_epi8
) 对于高 64 位, 因为 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
的错过优化问题。)
当然这需要另一个寄存器中的洗牌控制向量,所以只有当你可以多次使用它时才值得。
vpshufb
可用,因为每个车道所需的数据从广播中就已经存在,并且shuffle-control的高位会将相应的元素归零。
这个broadcast+shuffle的策略在Ryzen上可能还不错; Agner Fog 没有在上面列出 vpmovsx/zx ymm
的 uop 计数。
不要 执行类似 128 位或 256 位加载的操作,然后将其打乱以进一步提供 vpmovzx
指令。总洗牌吞吐量可能已经成为瓶颈,因为 vpmovzx
是洗牌。 Intel Haswell/Skylake(最常见的 AVX2 uarches)每时钟 1 次随机播放,但每时钟 2 次加载。使用额外的洗牌指令而不是将单独的内存操作数折叠成 vpmovzxbd
是很糟糕的。只有你可以像我建议的那样 减少 总 uop 数量 broadcast-load + vpmovzxbd + vpshufb 它是一个胜利。
我在 上的回答可能与转换回 uint8_t
相关。如果使用 AVX2 packssdw/packuswb
,后面的 pack-back-to-bytes 部分是半棘手的,因为它们在通道内工作,不像 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
负载,然后是一个带有 XMM 输入的单独 vpmovzx
。 (使用 ICC 和 clang3.6+ 我们可以通过使用 _mm_loadl_epi64
获得安全 + 最佳代码,就像 gcc9+ 一样)
但是 gcc8.3 和更早版本 可以 将一个 _mm_loadu_si128
16 字节加载内在函数折叠到一个 8 字节内存操作数中。这在 GCC 上的 -O3
处提供了最佳的 asm,但在 -O0
处是不安全的,它编译为实际的 vmovdqu
加载,涉及我们实际加载的更多数据,并且可能会结束一页。
由于这个答案提交了两个 gcc 错误:
- SSE/AVX movq load (_mm_cvtsi64_si128) not being folded into pmovzx(已为 gcc9 修复,但此修复会破坏 128 位加载的加载折叠,因此针对旧 GCC 的解决方法 hack 会使 gcc9 性能更差。)
- No intrinsic for x86
MOVQ m64, %xmm
in 32bit mode。 (TODO:也为 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 的版本对于 reader 来说是一项无趣的练习。您要求 "instructions",而不是 "intrinsics",这是内在函数中存在差距的地方。必须使用 _mm_cvtsi64_si128
来避免可能从越界地址加载是愚蠢的,IMO。我希望能够根据它们映射到的指令来考虑内在函数,使用 load/store 内在函数来通知编译器对齐保证或缺乏对齐保证。必须将内在函数用于我不想要的指令是非常愚蠢的。
另请注意,如果您正在查看英特尔 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一样load/store。 MOVQ xmm, xmm/m64)
.
的操作码 F3 0F 7E
(VEX.128.F3.0F.WIG 7E
)
asm ISA 参考手册仅列出了 m128i _mm_mov_epi64(__m128i a)
内在函数,用于在复制向量时将向量的高位 64b 归零。但是 the intrinsics guide does list _mm_loadl_epi64(__m128i const* mem_addr)
有一个愚蠢的原型(当它实际上只加载 8 个字节时指向 16 字节 __m128i
类型)。它在所有 4 个主要的 x86 编译器上都可用,而且实际上应该是安全的。请注意,__m128i*
只是传递给这个不透明的内在函数, 而不是 实际上是取消引用。
还列出了更健全的_mm_loadu_si64 (void const* mem_addr)
,但是gcc缺少那个。
我正在优化图像上的高斯模糊算法,我想用 __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 位整数。从那里,可以就地转换为浮点数。
; 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
(_mm256_shuffle_epi8
) 对于高 64 位, 因为 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
的错过优化问题。)
当然这需要另一个寄存器中的洗牌控制向量,所以只有当你可以多次使用它时才值得。
vpshufb
可用,因为每个车道所需的数据从广播中就已经存在,并且shuffle-control的高位会将相应的元素归零。
这个broadcast+shuffle的策略在Ryzen上可能还不错; Agner Fog 没有在上面列出 vpmovsx/zx ymm
的 uop 计数。
不要 执行类似 128 位或 256 位加载的操作,然后将其打乱以进一步提供 vpmovzx
指令。总洗牌吞吐量可能已经成为瓶颈,因为 vpmovzx
是洗牌。 Intel Haswell/Skylake(最常见的 AVX2 uarches)每时钟 1 次随机播放,但每时钟 2 次加载。使用额外的洗牌指令而不是将单独的内存操作数折叠成 vpmovzxbd
是很糟糕的。只有你可以像我建议的那样 减少 总 uop 数量 broadcast-load + vpmovzxbd + vpshufb 它是一个胜利。
我在 uint8_t
相关。如果使用 AVX2 packssdw/packuswb
,后面的 pack-back-to-bytes 部分是半棘手的,因为它们在通道内工作,不像 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
负载,然后是一个带有 XMM 输入的单独 vpmovzx
。 (使用 ICC 和 clang3.6+ 我们可以通过使用 _mm_loadl_epi64
获得安全 + 最佳代码,就像 gcc9+ 一样)
但是 gcc8.3 和更早版本 可以 将一个 _mm_loadu_si128
16 字节加载内在函数折叠到一个 8 字节内存操作数中。这在 GCC 上的 -O3
处提供了最佳的 asm,但在 -O0
处是不安全的,它编译为实际的 vmovdqu
加载,涉及我们实际加载的更多数据,并且可能会结束一页。
由于这个答案提交了两个 gcc 错误:
- SSE/AVX movq load (_mm_cvtsi64_si128) not being folded into pmovzx(已为 gcc9 修复,但此修复会破坏 128 位加载的加载折叠,因此针对旧 GCC 的解决方法 hack 会使 gcc9 性能更差。)
- No intrinsic for x86
MOVQ m64, %xmm
in 32bit mode。 (TODO:也为 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 的版本对于 reader 来说是一项无趣的练习。您要求 "instructions",而不是 "intrinsics",这是内在函数中存在差距的地方。必须使用 _mm_cvtsi64_si128
来避免可能从越界地址加载是愚蠢的,IMO。我希望能够根据它们映射到的指令来考虑内在函数,使用 load/store 内在函数来通知编译器对齐保证或缺乏对齐保证。必须将内在函数用于我不想要的指令是非常愚蠢的。
另请注意,如果您正在查看英特尔 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一样load/store。
的操作码MOVQ xmm, xmm/m64)
.F3 0F 7E
(VEX.128.F3.0F.WIG 7E
)asm ISA 参考手册仅列出了
m128i _mm_mov_epi64(__m128i a)
内在函数,用于在复制向量时将向量的高位 64b 归零。但是 the intrinsics guide does list_mm_loadl_epi64(__m128i const* mem_addr)
有一个愚蠢的原型(当它实际上只加载 8 个字节时指向 16 字节__m128i
类型)。它在所有 4 个主要的 x86 编译器上都可用,而且实际上应该是安全的。请注意,__m128i*
只是传递给这个不透明的内在函数, 而不是 实际上是取消引用。还列出了更健全的
_mm_loadu_si64 (void const* mem_addr)
,但是gcc缺少那个。