如果您使用的是 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 位, 因为英特尔 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 没有列出 vpmovsx/zx ymm 的 uop 计数。
不执行 128 位或 256 位加载之类的操作,然后随机播放以提供更多 vpmovzx 指令。总 shuffle 吞吐量可能已经成为瓶颈,因为 vpmovzx 是一个 shuffle。 Intel Haswell/Skylake(最常见的 AVX2 uarches)每时钟 1 次随机播放,但每时钟 2 次加载。使用额外的 shuffle 指令而不是将单独的内存操作数折叠到 vpmovzxbd 是很糟糕的。只有像我建议的广播负载 + vpmovzxbd + vpshufb 那样,您可以减少 uop 总数,这才是胜利。
我对@987654322@ 的回答可能与转换回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 负载,然后是一个带有 XMM 输入的单独的 vpmovzx。 (使用 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 7E (VEX.128.F3.0F.WIG 7E) 为MOVQ xmm, xmm/m64)。
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 缺少那个。