全零:pxor xmm0,xmm0(或xorps xmm0,xmm0,缩短一个指令字节。)在现代 CPU 上没有太大区别,但在 Nehalem 上(在消除异或零之前),xorps uop 只能运行在端口 5 上。我认为这就是为什么编译器支持 pxor-zeroing 的原因,即使对于将与 FP 指令一起使用的寄存器也是如此。
全一:pcmpeqw xmm0,xmm0。这是生成其他常量的通常起点,因为(如pxor)它打破了对寄存器先前值的依赖(除了旧 CPU,如 K10 和 pre-Core2 P6)。
在 Agner Fog 的指令表中的任何 CPU 上,W 版本与 pcmpeq 的字节或双字元素大小版本相比没有优势,但 pcmpeqQ 需要一个额外的字节,在 Silvermont 上速度较慢,并且需要 SSE4 .1.
所以doesn't really have table formatting,所以我只列出 Agner Fog 表 13.10 的新增内容,而不是改进版本。对不起。也许如果这个答案变得流行起来,我会使用 ascii-art 表格生成器,但希望改进将纳入指南的未来版本。
Agner Fog 的表生成 16 位元素的向量,并使用 packuswb 来解决这个问题。例如,pcmpeqw xmm0,xmm0/psrlw xmm0,15/psllw xmm0,1/packuswb xmm0,xmm0 生成一个向量,其中每个字节都是2。 (这种具有不同计数的移位模式是为更宽的向量生成大多数常量的主要方式)。有更好的办法:
paddb xmm0,xmm0 (SSE2) 以字节为单位左移一格,因此-2 字节的向量 只需两条指令即可生成(pcmpeqw / @987654349 @)。 paddw/d/q 作为其他元素大小的左移一个,与移位相比节省了一个字节的机器代码,并且通常可以在比 shift-imm 更多的端口上运行。
pabsb xmm0,xmm0 (SSSE3) 将全一向量 (-1) 转换为 1 字节向量,并且是非破坏性的,因此您仍然拥有 set1(-1)向量。
(您有时不需要set1(1)。您可以通过用psubb 减去-1 来为每个元素加1。)
我们可以用pcmpeqw/paddb/pabsb生成2字节。 (add 与 abs 的顺序无关紧要)。 pabs 不需要 imm8,但仅在其他元素宽度和右移都需要 3 字节 VEX 前缀时保存代码字节。这仅在源寄存器为 xmm8-15 时发生。 (vpabsb/w/d 总是要求 VEX.128.66.0F38.WIG 使用 3 字节 VEX 前缀,但 vpsrlw dest,src,imm 可以使用 2 字节 VEX 前缀作为其 VEX.NDD.128.66.0F.WIG)。
我们实际上也可以在生成4字节时保存指令:pcmpeqw/pabsb/psllw xmm0, 2。由于pabsb,所有通过字移位跨越字节边界移位的位都为零。显然,其他移位计数可以将单个设置位放在其他位置,包括符号位以生成 -128 (0x80) 字节的向量。请注意,pabsb 是非破坏性的(目标操作数是只写的,不需要与源操作数相同即可获得所需的行为)。您可以将全一保留为常量,或作为生成另一个常量的开始,或作为psubb 的源操作数(递增一)。
0x80 字节的向量 也可以使用packsswb 从饱和到-128 的任何内容生成(参见上一段)。例如如果您已经有一个向量 0xFF00 用于其他内容,只需复制它并使用 packsswb。从内存中加载而恰好正确饱和的常量是这方面的潜在目标。
可以使用pcmpeqw / psrlw xmm0, 9 / packuswb xmm0,xmm0 生成一个0x7f 字节的向量。我认为这是“不明显的”,因为大多数设置的性质并没有让我想到只是在每个单词中生成它作为一个值并执行通常的packuswb。
pavgb (SSE2) 针对零寄存器可以右移一位,但前提是该值是偶数。 (它确实无符号 dst = (dst+src+1)>>1 用于舍入,临时使用 9 位内部精度。)但这似乎对常量生成没有用,因为 0xff 是奇数:pxor xmm1,xmm1 / pcmpeqw xmm0,xmm0 / @ 987654387@ / pavgb xmm0, xmm1 产生 0x7f 字节 比 shift/pack 多一个 insn。但是,如果其他东西已经需要一个归零寄存器,paddb / pavgb 确实会节省一个指令字节。
我已经测试了这些序列。最简单的方法是将它们放入.asm,组装/链接,然后在其上运行 gdb。 layout asm、display /x $xmm0.v16_int8 在每个单步和单步指令(ni 或 si)之后转储。在layout reg 模式下,您可以使用tui reg vec 切换到矢量reg 的显示,但这几乎没用,因为您无法选择要显示的解释(您总是得到所有这些,并且无法hscroll,并且列不在寄存器之间排列)。不过,它非常适合整数 regs/flags。
请注意,将这些与内在函数一起使用可能会很棘手。编译器不喜欢对未初始化的变量进行操作,所以你应该使用_mm_undefined_si128() 告诉编译器你的意思。或者也许使用_mm_set1_epi32(-1) 会让你的编译器发出一个pcmpeqd same,same。没有这个,一些编译器会在使用前对未初始化的向量变量进行异或归零,甚至(MSVC)从堆栈中加载未初始化的内存。
通过利用 SSE4.1 的 pmovzx 或 pmovsx 动态零或符号扩展,许多常量可以更紧凑地存储在内存中。例如,{1, 2, 3, 4} 作为 32 位元素的 128b 向量可以通过从 32 位内存位置加载 pmovzx 来生成。内存操作数可以与pmovzx 进行微融合,因此不需要任何额外的融合域微指令。不过,它确实阻止了将常量直接用作内存操作数。
C/C++ intrinsics support for using pmovz/sx as a load is terrible:有_mm_cvtepu8_epi32 (__m128i a),但没有采用uint32_t * 指针操作数的版本。你可以绕过它,但它很丑陋,编译器优化失败是一个问题。有关详细信息和 gcc 错误报告的链接,请参阅链接问题。
使用 256b 和(并非如此)不久的 512b 常量,内存中的节省会更大。不过,只有当多个有用的常量可以共享一个缓存行时,这才非常重要。
与此等效的 FP 是 VCVTPH2PS xmm1, xmm2/m64,需要 F16C(半精度)功能标志。 (还有一条存储指令,将单个压缩到一半,但没有半精度计算。它只是内存带宽/缓存占用优化。)
显然,当所有元素都相同(但不适合动态生成)时,pshufd 或 AVX vbroadcastps / AVX2 vpbroadcastb/w/d/q/i128 很有用。 pshufd 可以采用内存源操作数,但必须是 128b。 movddup (SSE3) 进行 64 位加载,广播以填充 128b 寄存器。在 Intel 上,它不需要 ALU 执行单元,只需要加载端口。 (类似地,AVX v[p]broadcast dword 大小和更大的负载在负载单元中处理,没有 ALU)。
广播或pmovz/sx 非常适合节省可执行文件大小,当您要将掩码加载到寄存器中以在循环中重复使用时。如果只需要一条指令,从一个起点生成多个相似的掩码也可以节省空间。
另请参阅For for an SSE vector that has all the same components, generate on the fly or precompute?,它询问有关使用 set1 内在函数的更多信息,不清楚它是否询问常量或变量的广播。
我还用compiler output for broadcasts做了一些实验。
如果缓存未命中是个问题,请查看您的代码,看看当同一个函数被内联到不同的调用者时,编译器是否重复了 _mm_set 常量。还要注意一起使用的常量(例如,在一个接一个调用的函数中)分散到不同的缓存行中。常量的许多分散加载远比加载大量彼此靠近的常量要糟糕得多。
pmovzx 和/或广播加载允许您将更多常量打包到缓存行中,将它们加载到寄存器中的开销非常低。负载不会在关键路径上,因此即使需要额外的 uop,它也可以在长窗口的任何周期中占用一个空闲的执行单元。
clang actually does a good job of this:不同函数中的单独set1常量被识别为相同,可以合并相同字符串文字的方式。请注意,clang 的 asm 源输出似乎显示每个函数都有自己的常量副本,但二进制反汇编显示所有这些 RIP 相关的有效地址都引用相同的位置。对于 256b 版本的重复函数,clang 还使用vbroadcastsd 只需要 8B 加载,代价是每个函数中都需要额外的指令。 (这是-O3,很明显clang 开发人员已经意识到大小对性能很重要,而不仅仅是-Os)。 IDK 为什么它不会用vbroadcastss 降到 4B 常数,因为那应该一样快。不幸的是,vbroadcast 不只是来自其他函数使用的 16B 常量的一部分。这可能是有道理的:某物的 AVX 版本可能只能将其一些常量与 SSE 版本合并。最好让具有 SSE 常量的内存页面完全冷却,并让 AVX 版本将其所有常量放在一起。此外,在汇编或链接时处理模式匹配问题是一个更难的问题(无论如何已经完成了。我没有阅读每个指令来确定哪个指令启用了合并。)
gcc 5.3 也合并常量,但不使用广播加载来压缩 32B 常量。同样,16B 常数不与 32B 常数重叠。