"最佳"表示最少的指令(或最少的uops,如果任何指令解码到多个uop)。机器码大小(以字节为单位)是相同insn计数的平局。
恒定生成本质上是一个新的依赖关系链的开始,所以它对于延迟至关重要。在循环中生成常量也很常见,因此吞吐量和执行端口需求也几乎无关紧要。
生成常量而不是加载它们需要更多指令(除了全零或全一),因此它会占用宝贵的uop-cache空间。这可能是比数据缓存更有限的资源。
Agner Fog的优秀Optimizing Assembly guide涵盖Section 13.4
。表13.10包含用于生成向量的序列,其中每个元素都是0
,1
,2
,3
,4
,-1
或{{ 1}},元素大小为8到64位。表13.11包含用于生成一些浮点值的序列(-2
,0.0
,0.5
,1.0
,1.5
,2.0
和位掩码标志位。)
Agner Fog的序列仅使用SSE2,无论是设计还是因为它已经有一段时间没有更新。
使用短的非显而易见的指令序列可以生成哪些其他常量?(具有不同移位计数的进一步扩展是显而易见的而不是"有趣"。)是否有更好的序列为了生成常数Agner Fog列出了什么?
How to move 128-bit immediates to XMM registers说明了将任意128b常量放入指令流的一些方法,但这通常不合理(它不会节省任何空间,并占用大量的uop-cache空间。)
答案 0 :(得分:22)
全零:pxor xmm0,xmm0
(或xorps xmm0,xmm0
,缩短一个指令字节。)
全能:pcmpeqw xmm0,xmm0
。这是生成其他常量的常用起点,因为(如pxor
)它打破了对寄存器先前值的依赖性(除了旧的CPU,如K10和前Core2 P6)。在{Agner Fog的指令表中的任何CPU上,W
版本的pcmpeq
版本的pcmpeqQ
版本没有任何优势,但packuswb
需要额外字节,在Silvermont上较慢,并且需要SSE4.1。
SO doesn't really have table formatting,所以我只是列出Agner Fog的表13.10的补充内容,而不是改进的版本。抱歉。也许如果这个答案变得流行,我将使用ascii-art表格生成器,但希望改进将被纳入该指南的未来版本。
PSLLB
Agner Fog的表生成16位元素的向量,并使用pcmpeqw xmm0,xmm0
来解决这个问题。例如,psrlw xmm0,15
/ psllw xmm0,1
/ packuswb xmm0,xmm0
/ 2
生成一个向量,其中每个字节都是paddb xmm0,xmm0
。 (这种移位模式,具有不同的计数,是产生更宽向量的大多数常数的主要方式)。有一种更好的方法:
-2
(SSE2)作为左移一个字节粒度,因此只需两条指令即可生成 pcmpeqw
字节的向量({{ 1}} / paddb
)。 paddw/d/q
作为其他元素大小的左移一个节省了一个字节的机器代码与移位相比,并且通常可以在比shift-imm更多的端口上运行。
pabsb xmm0,xmm0
(SSSE3)将全1(-1
)的向量转换为1
字节的向量,因此只需要两条指令。我们可以使用2
/ pcmpeqw
/ paddb
生成 pabsb
字节。 (添加与绝对的顺序并不重要)。 pabs
不需要imm8,但只需要保存其他元素宽度的代码字节而不需要3字节VEX前缀时右移。这仅在源寄存器为xmm8-15时发生。 (vpabsb/w/d
始终需要VEX.128.66.0F38.WIG
的3字节VEX前缀,但vpsrlw dest,src,imm
可以为其VEX.NDD.128.66.0F.WIG
使用2字节VEX前缀。
我们实际上也可以在生成 4
字节时保存说明:pcmpeqw
/ pabsb
/ psllw xmm0, 2
。由于pabsb
,所有通过字移位跨字节边界移位的位都为零。显然,其他移位计数可以将单个设置位置于其他位置,包括符号位以生成 -128(0x80)字节的向量。请注意,pabsb
是非破坏性的(目标操作数是只写的,并且不需要与源相同才能获得所需的行为)。你可以将all-ones作为常量保持,或者作为生成另一个常量的开始,或者作为psubb
的源操作数(增加1)。
0x80
字节的向量也可以(使用packsswb
从饱和到-128的任何内容生成(参见上一段)。例如如果您已经为其他内容添加了0xFF00
向量,只需复制它并使用packsswb
即可。从内存中加载的正常情况下正常饱和的常量是这个的潜在目标。
可以使用0x7f
/ pcmpeqw
/ psrlw xmm0, 9
生成 packuswb xmm0,xmm0
字节的向量。我认为这是"非显而易见的"因为大多数设定的性质并没有让我想到只是将它作为每个单词中的值生成并执行通常的packuswb
。
pavgb
(SSE2)可以右移1,但仅在值为偶数时才会右移。 (它使用无符号dst = (dst+src+1)>>1
进行舍入,临时内部精度为9位。)但这对于常量生成似乎没有用,因为0xff是奇数:pxor xmm1,xmm1
/ pcmpeqw xmm0,xmm0
/ paddb xmm0,xmm0
/ pavgb xmm0, xmm1
生成 0x7f
字节,其中一个insn比shift / pack更多。但是,如果其他东西已经需要归零寄存器,paddb
/ pavgb
会保存一个指令字节。
我测试了这些序列。最简单的方法是将它们放入.asm
,汇编/链接,并在其上运行gdb。 layout asm
,display /x $xmm0.v16_int8
在每个单步和单步说明(ni
或si
)之后转储。在layout reg
模式下,您可以tui reg vec
切换到矢量注册表的显示,但它几乎无用,因为您无法选择要显示的解释(您始终可以获得所有解释)其中,并且不能滚动,列不在寄存器之间排列)。但它对于整数regs / flags来说非常好。
请注意,将这些与内在函数一起使用可能会非常棘手。编译器不喜欢对未初始化的变量进行操作,因此您应该使用_mm_undefined_si128()
告诉编译器您的意思。或者使用_mm_set1_epi32(-1)
可能会让编译器发出pcmpeqd same,same
。如果没有这个,一些编译器会在使用之前对未初始化的矢量变量进行xor-zero,甚至(MSVC)从堆栈中加载未初始化的内存。
通过利用SSE4.1的pmovzx
或pmovsx
进行零或符号扩展,可以将更多紧凑的常量存储在内存中。例如,可以使用来自32位存储器位置的{1, 2, 3, 4}
负载生成作为32位元素的pmovzx
的128b向量。内存操作数可以与pmovzx
微融合,因此它不会占用任何额外的融合域uops。但它确实阻止将常量直接用作内存操作数。
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寄存器。在英特尔,它不需要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常数不重叠。