“最佳”意味着最少的指令(或最少的微操作,如果任何指令解码为一个以上的微操作)。以字节为单位的机器代码大小是相同的insn计数的决胜因素。
常量生成本质上是一个新依赖链的开始,因此延迟很重要,在循环中生成常量也很不常见,因此吞吐量和执行端口需求也几乎无关紧要。
生成常量而不是加载它们需要更多的指令(除了全零和全一),因此它消耗了宝贵的uop缓存空间,这可能是比数据缓存更有限的资源。
Agner Fog的优秀的Optimizing Assembly guide在Section 13.4
中涵盖了这一点,表13 - 10给出了生成向量的序列,其中每个元素都是0
、1
、2
、3
、4
、-1
或-2
。元素大小从8位到64位。表13.11给出了生成一些浮点值的序列(0.0
、0.5
、1.0
、1.5
、2.0
、-2.0
以及符号位的位掩码)。
Agner Fog的序列只使用SSE 2,无论是设计还是因为它有一段时间没有更新。
还有哪些常量可以用不明显的短指令序列生成?(使用不同移位计数的进一步扩展是明显的,并不“有趣”。)有更好的序列来生成Agner Fog列出的常量吗?
How to move 128-bit immediates to XMM registers说明了一些将任意128 b常量放入指令流的方法,但这通常是不明智的(它不保存任何空间,并且占用大量uop缓存空间)。
1条答案
按热度按时间mhd8tkvw1#
全零:
pxor xmm0,xmm0
(或xorps xmm0,xmm0
,短了一个指令字节)。在现代CPU上没有太大区别,但在Nehalem上(在异或零消除之前),xorps uop只能在端口5上运行。我想这就是为什么编译器喜欢pxor
-零化,即使是将与FP指令一起使用的寄存器。pcmpeqw xmm0,xmm0
。这是生成其他常量的常见起点,因为(如pxor
)它打破了对寄存器先前值的依赖性(K10和Pre-Core 2 P6等旧CPU除外)。在Agner Fog的指令表中,
W
版本在任何CPU上都没有比pcmpeq
的字节或双字元素大小版本更大的优势,但pcmpeqQ
需要额外的字节,在Silvermont上速度更慢,并且需要SSE4.1。所以doesn't really have table formatting,所以我只列出Agner Fog的表13.10的补充,而不是改进版本。抱歉,如果这个答案流行起来,我可能会使用一个ascii艺术表生成器,但希望改进将滚动到指南的未来版本中。
主要的困难是8位向量,因为there's no
PSLLB
Agner Fog的表生成16位元素的向量,并使用
packuswb
来解决这个问题,例如,pcmpeqw xmm0,xmm0
/psrlw xmm0,15
/psllw xmm0,1
/packuswb xmm0,xmm0
生成一个向量,其中每个字节都是2
。(这种移位模式,使用不同的计数,是为更宽的向量产生大多数常数的主要方法)。还有一种更好的方法:paddb xmm0,xmm0
(SSE 2)用作具有字节粒度的左移1,因此-2
字节的向量可仅用两个指令(pcmpeqw
/paddb
)来产生。paddw/d/q
用作用于其它元素大小的左移1与移位相比节省机器代码的一个字节,且通常可在比shift-imm多的端口上运行。pabsb xmm0,xmm0
(SSSE 3)将全1向量(-1
)转换为1
字节的向量,并且是非破坏性的,因此您仍然可以使用set1(-1)
向量。(You有时不需要
set1(1)
,你可以用psubb
减去-1
来给每个元素加1。)我们可以使用
pcmpeqw
/paddb
/pabsb
生成2
字节**。(add与abs的顺序无关紧要)。pabs
不需要imm 8,但在其他元素宽度和右移都需要3字节VEX前缀时,只会为它们节省代码字节。这仅在源寄存器为xmm 8 -15时发生。(vpabsb/w/d
总是要求VEX.128.66.0F38.WIG
使用3字节的VEX前缀,但是vpsrlw dest,src,imm
可以使用2字节的VEX前缀)。实际上,生成
4
等2的幂字节时,我们也可以保存指令:pcmpeqw
/pabsb
/psllw xmm0, 2
。由于pabsb
,通过字移位跨越字节边界移位的所有位均为零。显然,其它移位计数可将单个设置位置于其它位置,包括符号位以生成向量**-128(0x 80)字节**。请注意,pabsb
是非破坏性的(目标操作数是只写的,不需要与源操作数相同即可获得所需的行为)。您可以将全1作为常量保留,或者作为生成另一个常量的开始,或者作为X1 M42 N1 X的源操作数(递增1)。一个
0x80
字节的向量也可以(见上一段)从饱和到-128的任何东西生成,使用packsswb
。例如,如果你已经有一个0xFF00
的向量用于其他东西,只需复制它并使用packsswb
。从内存加载的碰巧饱和的常量是这个的潜在目标。0x7f
字节的向量可以用pcmpeqw
/paddb xmm0,xmm0
/psrlw xmm0, 1
生成。这比pcmpeqw
/psrlw xmm0, 9
/packuswb xmm0,xmm0
稍微好一点,psrlw xmm0, 9
/packuswb xmm0,xmm0
是在每个字中生成值并使用packuswb
的常用技巧。但是在大多数CPU上,PADDB比PACK可以在更多的端口上运行。pavgb
(SSE 2)对置零寄存器可以右移一位,但仅当值为偶数时。(它对无符号dst = (dst+src+1)>>1
进行舍入,临时值的内部精度为9位。)但这似乎对常量生成没有用处,因为0xff是奇数:pxor xmm1,xmm1
/pcmpeqw xmm0,xmm0
/paddb xmm0,xmm0
/pavgb xmm0, xmm1
生成0x7f
字节**,其中insn比移位/压缩多一个。不过,如果其他操作已经需要置零寄存器,则paddb
/pavgb
会保存一个指令字节。我已经测试过这些序列,最简单的方法是将它们放入一个
.asm
中,进行汇编/链接,然后在其上运行gdb.layout asm
,display /x $xmm0.v16_int8
,以便在每个单步和单步指令之后转储它(ni
或si
)。在layout reg
模式下,您可以执行tui reg vec
切换到矢量寄存器的显示,但它几乎毫无用处,因为您无法选择要显示的解释(你总是能得到所有的寄存器,而且不能hscroll,寄存器之间的列也不会对齐)。不过,这对于整型寄存器/标志来说是非常好的。注意,在intrinsic中使用这些变量可能会很棘手。编译器不喜欢操作未初始化的变量,所以你应该使用
_mm_undefined_si128()
来告诉编译器你的意思。或者使用_mm_set1_epi32(-1)
会让你的编译器发出pcmpeqd same,same
。如果没有这个,一些编译器会在使用前对未初始化的向量变量进行异或零运算,甚至(MSVC)从堆栈中加载未初始化的内存。通过利用SSE4.1的
pmovzx
或pmovsx
进行零或符号扩展,许多常量可以更紧凑地存储在内存中。例如,{1, 2, 3, 4}
作为32位元素的128 b向量可以通过从32位内存位置加载pmovzx
来生成。内存操作数可以与pmovzx
微融合,所以它不会接受任何额外的融合域微操作,但是它确实防止了直接将常量用作内存操作数。C/ C++ intrinsics support for using
pmovz/sx
as a load is terrible:有_mm_cvtepu8_epi32 (__m128i a)
,但是没有接受uint32_t *
指针操作数的版本。你可以绕过它,但是它很难看,并且编译器优化失败是个问题。查看链接的问题了解详细信息,并链接到gcc bug报告。有了256 b和512 b常量(不太快),节省的内存空间更大,但只有当多个有用的常量可以共享一个缓存行时,这一点才非常重要。
与此对应的FP指令是
VCVTPH2PS xmm1, xmm2/m64
,需要F16 C(半精度)特性标志(还有一条存储指令,它将单精度压缩为半精度,但不进行半精度计算,它只是一个内存带宽/缓存占用优化)。显然当所有元素都相同时(但不适合动态生成)、
pshufd
或AVXvbroadcastps
/AVX 2vpbroadcastb/w/d/q/i128
都很有用。pshufd
可以接受内存源操作数,但必须是128 b。movddup
(SSE 3)执行64位加载,广播以填充128 b寄存器。在Intel上,它不需要ALU执行单元,只需要加载端口。(类似地,AVXv[p]broadcast
双字长及更大的加载在加载单元中处理,不需要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实际上在这方面做得很好:不同函数中单独的
set1
常量被认为是相同的,就像相同的字符串可以合并一样。注意,clang的asm源代码输出似乎显示每个函数都有自己的常量副本,但二进制反汇编显示所有这些RIP相关的有效地址都引用同一个位置。对于重复函数的256 b版本,clang还使用vbroadcastsd
来只需要8B的负载,代价是每个函数中有一条额外的指令。(这是在-O3
上,所以很明显,clang开发人员已经意识到大小对性能很重要,而不仅仅是-Os
)IDK为什么vbroadcastss
不降到4 B常量,因为那样应该一样快,不幸的是,vbroadcast不是简单的来自其他函数使用的16 B常量的一部分,这也许是有意义的:某个东西的AVX版本可能只能将它的一些常量与SSE版本合并。最好让带有SSE常量的内存页完全保持冷态,并让AVX版本将它的所有常量保持在一起。此外,在汇编或链接时,这是一个更难处理的模式匹配问题(无论如何完成。我没有阅读每一个指令来确定哪个指令启用合并)。gcc 5.3也合并了常量,但是没有使用广播加载来压缩32 B常量。同样,16 B常量没有与32 B常量重叠。
GCC 12已经开始倾向于在AVX可用的情况下动态构建一些矢量常数,但从
mov reg, imm64
/vmovq
/ shuffle开始。它将使用庞大64位立即数和X1 M100 N1 X,而不是5字节X1 M101 N1 X和双字广播。同样具有讽刺意味的是,没有为set1_epi16(0x00ff)
动态构建,这是微不足道的(pcmpeqd
/psrlw xmm, 8
)。https://godbolt.org/z/78cMaxjMz在AVX-512可用的情况下,
mov r,imm
/vpbroadcastd x/y/zmm, eax
仅为2条指令(vpbroadcastd ymm0, eax
在Intel上为1 uop,在Zen 4上为2 uop),这使得以这种方式构造向量更具吸引力,并且编译器更容易实现。