全零: pxor xmm0,xmm0
(或者 xorps xmm0,xmm0
, 只是指令字节更短一点). 在现代CPU上没有太大区别, 但在 Nehalem 上(在 xor-zero 消除之前), xorps uop 只能在端口5上运行. 我认为这就是为什么编译器喜欢使用 pxor
-zeroing 即使对于将与 FP 指令一起使用的寄存器也是如此.
全一: pcmpeqw xmm0,xmm0
. 这是生成其他常量的常用起始点,因为它(像 pxor
一样)打破了对寄存器先前值的依赖(除了旧的 CPU 如 K10 和 pre-Core2 P6).
在 Agner Fog 的指令表中,W
版本与 pcmpeq
的字节或双字元素大小版本在任何 CPU 上都没有优势,但 pcmpeqQ
需要额外一个字节,在 Silvermont 上速度较慢并且需要 SSE4.1。
所以实际上并没有表格格式,所以我只会列出 Agner Fog 的表 13.10 的补充,而不是改进版本。 抱歉。也许如果这个答案变得流行起来,我会使用 ASCII 艺术表格生成器,但希望将来的指南版本会有所改进。
Agner Fog 的表生成了16位元素的向量,并使用 packuswb
来解决这个问题。例如,pcmpeqw xmm0,xmm0
/ psrlw xmm0,15
/ psllw xmm0,1
/ packuswb xmm0,xmm0
生成一个每个字节都是 2
的向量。(这种移位模式,具有不同的计数,是生成更宽向量的大多数常量的主要方法)。有一种更好的方法:
paddb xmm0,xmm0
(SSE2) 可以作为带有字节粒度的左移一位,因此只需要两个指令(pcmpeqw
/ paddb
)就可以生成一个向量为 -2
字节. 对于其他元素大小,paddw/d/q
作为左移一位比移位指令节省一个字节的机器代码,并且通常可以在更多的端口上运行。
pabsb xmm0,xmm0
(SSSE3)将一个全为1的向量(-1
)转换为一个1字节的向量,且不会破坏原有的set1(-1)
向量。
有时候您并不需要set1(1)
。您可以通过使用psubb
减去-1
来将每个元素加1。
我们可以通过pcmpeqw
/paddb
/pabsb
生成2个字节
的向量。(加法和绝对值操作的顺序并不重要)。pabs
不需要imm8,但是只有在源寄存器为xmm8-15时才能为其他元素宽度节省代码字节而不是右移。 (vpabsb/w/d
始终需要一个3字节的VEX前缀以进行VEX.128.66.0F38.WIG
,但是vpsrlw dest,src,imm
可以使用其VEX.NDD.128.66.0F.WIG
的2字节VEX前缀)。
实际上,我们还可以通过pcmpeqw
/pabsb
/psllw xmm0, 2
生成4
等2的幂次方字节,所有通过字移位跨越字节边界的位都是零,这要归功于pabsb
。显然,其他移位计数可以将单个设置位放在其他位置,包括符号位以生成一个-128(0x80)字节向量。请注意,pabsb
是非破坏性的(目标操作数仅写入,不需要与源相同即可获得所需行为)。您可以将全1保留为常量,或者作为生成另一个常量的起点,或者作为psubb
的源操作数(加1)。
也可以使用packsswb
从任何饱和到-128的值生成一个0x80
字节向量(参见上一段)。例如,如果您已经有一个用于其他用途的0xFF00
向量,请复制它并使用packsswb
。从内存加载的常量如果恰好饱和,则可能成为此类操作的潜在目标。
使用
pcmpeqw
/
paddb xmm0,xmm0
/
psrlw xmm0, 1
可以生成一个包含
0x7f
字节的
向量。这比通常使用
pcmpeqw
/
psrlw xmm0, 9
/
packuswb xmm0,xmm0
生成每个字中值的技巧稍微更好。但是,在大多数CPU上,PADDB可以在比PACK更多的端口上运行。
使用
pavgb
(SSE2)与零寄存器相反,可以右移一位,但只有该值为偶数时才可以。(它对临时变量进行无符号的
dst = (dst + src + 1) >> 1
以进行四舍五入,并具有9位内部精度)。但似乎这对于常量生成不是很有用,因为0xff是奇数:
pxor xmm1,xmm1
/
pcmpeqw xmm0,xmm0
/
paddb xmm0,xmm0
/
pavgb xmm0, xmm1
生成的指令比移位和打包使用了一个更多的指令字节,但会产生相同的结果。但是,如果零寄存器已经被用于其他目的,则
paddb
/
pavgb
可以节省一个指令字节。
我已经测试了这些序列。最简单的方法是将它们放入
.asm
文件中,进行汇编链接,然后在gdb上运行它。
layout asm
,
display /x $xmm0.v16_int8
命令可在每个单步后转储其结果,并使用
ni
或
si
命令进行单步执行。在
layout reg
模式下,您可以使用
tui reg vec
命令切换到向量寄存器显示模式,但这几乎是无用的,因为无法选择要显示的解释(总是会得到所有的解释,不能水平滚动,而且列之间不对齐)。 但是,它对于整数寄存器/标志非常有用。
请注意,使用这些操作指令可能会很棘手。编译器不喜欢操作未初始化的变量,因此应使用
_mm_undefined_si128()
命令告诉编译器您的意思是什么。也许使用
_mm_set1_epi32(-1)
命令可以使编译器发出
pcmpeqd same,same
指令。如果没有这个命令,一些编译器将在使用之前对未初始化的向量变量进行异或零操作,甚至(MSVC)从堆栈中加载未初始化的内存。
许多常量可以通过利用SSE4.1的
pmovzx
或
pmovsx
在内存中更紧凑地存储,以进行零扩展或符号扩展。例如,一个由32位元素组成的128b向量
{1, 2, 3, 4}
可以通过从32位内存位置进行
pmovzx
加载生成。内存操作数可以与
pmovzx
微融合,因此不需要任何额外的融合域uops。但是,它会阻止直接将常量用作内存操作数。
C/C++中使用
pmovz/sx
作为加载的
内部支持非常糟糕:有
_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
内部函数的情况,不清楚它是否涉及常量或变量的广播。
我还对广播的编译器输出进行了一些实验。
如果缓存未命中是一个问题,请查看您的代码,并查看编译器在将相同的函数内联到不同的调用者时是否已复制了_mm_set
常量。同时要注意,一起使用的常量(例如,在一个接一个调用的函数中)被分散到不同的缓存行中。许多分散的常量加载比从靠近彼此的位置加载很多常量要糟糕得多。
pmovzx
和/或广播加载允许您将更多的常量打包到缓存行中,非常低的开销将它们加载到寄存器中。加载不会成为关键路径上的操作,因此即使需要额外的微操作,它也可以在长时间窗口内的任何周期中占用一个免费的执行单元。
clang实际上做得很好:不同函数中的
set1
常量被识别为相同,就像相同的字符串字面量可以合并一样。请注意,clang的汇编源代码输出似乎显示每个函数都有其自己的常量副本,但二进制反汇编显示所有那些RIP相对有效地址都引用同一个位置。对于重复函数的256b版本,clang还使用
vbroadcastsd
只需要一个8B加载,代价是每个函数多了一条指令。(这是在
-O3
下进行的,因此显然clang开发人员已经意识到大小对性能很重要,而不仅仅是对
-Os
.)我不知道为什么它不会用
vbroadcastss
将常量降至4B,因为速度应该一样快。不幸的是,vbroadcast并不简单地来自其他函数使用的16B常量的一部分。这也许是有道理的:某个东西的AVX版本可能只能将某些常量与SSE版本合并。最好让带着SSE常量的内存页面完全不活跃,并让AVX版本将所有常量保持在一起。此外,这是一个更难的模式匹配问题,需要在汇编或链接时间处理(无论是怎么做的,我都没有阅读每个指令来找出哪个指令启用了合并)。
GCC 5.3同样合并常量,但不使用广播加载来压缩32B常量。同时16B常量不会与32B常量重叠。
GCC12开始倾向于在AVX可用时动态构建某些矢量常量,但始终使用mov reg,imm64
/ vmovq
/ shuffle。即使模式简单且重复,它也将使用笨重的64位立即数和vpunpcklqdq
而不是5字节的mov eax,imm32
和字广播。与此反过来的是,对于一个微不足道的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
为1个微操作码,在Zen 4上为2个微操作码)。这使得用这种方式构建向量更具吸引力,对于编译器来说更容易实现。