已经有一个问题与此相关,但由于“含糊不清”而关闭,因此我开了一个新的问题 - 我找到了答案,也许它会帮助其他人。
问题是:如何编写一系列汇编代码以使用128位立即值(常量)初始化XMM寄存器?
我想补充一点,可以在Agner Fog的手册中阅读有关使用汇编生成各种常量的内容。链接:Optimizing subroutines in assembly language,Generating constants部分,第124页。
你可以像这样做,只需使用一个movaps
指令:
.section .rodata # put your constants in the read-only data section
.p2align 4 # align to 16 = 1<<4
LC0:
.long 1082130432
.long 1077936128
.long 1073741824
.long 1065353216
.text
foo:
movaps LC0(%rip), %xmm0
通常使用数据加载来加载它比将其嵌入指令流中更可取,特别是因为它需要多少条指令。这会导致CPU执行几个额外的uops,而这个任意常数无法通过几次移位从全1生成。
如果方便的话,您可以将常量放在即时编译的函数的前面或后面,而不是放在单独的部分中。但由于CPU具有分离的L1d/L1i高速缓存和TLB,因此通常最好将常量分组到指令之外。
如果常量的两半相同,则可以使用SSE3进行广播加载
movddup (m64),%xmm0
。
.align
指令需要一个2的幂次方参数,因此.align 4
表示对其到2 ^ 4 = 16字节的倍数。 - Paul R.p2align 4
是一个不错的选择。它总是表示2的幂对齐,并且被引入是为了防止在不同的汇编器(或同一汇编器的不同版本)中,.align
表示不同的含义。我认为它已经存在比 SSE 更长时间了,因此应该安全地推荐使用它。 - Peter Cordes作为10,000种实现之一,使用SSE4.1 pinsrq
mov rax, first half
movq xmm0, rax ; better than pinsrq xmm0,rax,0 for performance and code-size
mov rax, second half
pinsrq xmm0, rax, 1
pinsertq
的文档在哪里?我在英特尔指令手册中找不到这个指令。 - Sergey L.movq
指令不允许将通用寄存器作为第二个操作数。因此它只是在无法快速汇编时才会更“快”。好的一面是, pinsrq
技巧可以使用。 - David Wohlferdmovq
有两种形式:您可能在想MOVQ xmm1,xmm2/m64
,它可以在32位或64位模式下汇编。但是这当然是使用MOVQ xmm,r/m64
形式,它是REX+MOVD,仅在64位模式下可用。显然,一些汇编器仍将其称为movd
,因此如果无法汇编,请尝试movd xmm0,rax
。或者更好的方法是使用movdqa
加载常量。 - Peter Cordes最佳解决方案(特别是如果您想坚持使用SSE2-即避免使用AVX)是使用MOVLHPS xmm0,xmm1将两个寄存器(如xmm0和xmm1)初始化为您的立即值的两个64位半部分。 为了初始化64位值,最简单的解决方案是使用通用寄存器(例如AX),然后使用MOVQ将其值传输到XMM寄存器。 因此,序列可能如下所示:
MOV RAX, <first_half>
MOVQ XMM0, RAX
MOV RAX, <second_half>
MOVQ XMM1, RAX
MOVLHPS XMM0,XMM1
PINSRQ xmm0,rax,1
可以替换movq
/movlhps
。此外,你应该说RAX,而不仅仅是AX。AX具体指RAX的低16位。你可以称之为A,但那只会让人感到困惑。无论如何,这比使用加载指令加载要糟糕。 - Peter Cordes在指令流中嵌入常量有多种方法:
因此,虽然没有办法将常量直接加载到XMM
寄存器中,但可以从代码执行的“旁边”加载一个PC相对地址(在64位中)。这样就会创建类似于以下内容:
.align 4
.val:
.long 0x12345678
.long 0x9abcdef0
.long 0xfedbca98
.long 0x76543210
func:
movdqa .val(%rip), %xmm0
当您进行反汇编时:
0000000000000000 : 0: 78 56 34 12 f0 de bc 9a 8: 98 ca db fe 10 32 54 76
0000000000000010 : 10: 66 0f 6f 05 e8 ff ff movdqa -0x18(%rip),%xmm0 # 0
这是非常紧凑的,只有23个字节。
其他选项是在堆栈上构建值,然后再从堆栈中加载它。在32位x86中,您没有%rip
相对内存访问,但仍可以在24个字节(假设堆栈指针在入口处对齐;否则,需要进行未对齐的加载)中完成此操作:
00000000 : 0: 68 78 56 34 12 push $0x12345678 5: 68 f0 de bc 9a push $0x9abcdef0 a: 68 98 ca db fe push $0xfedbca98 f: 68 10 32 54 76 push $0x76543210 14: 66 0f 6f 04 24 movdqa (%esp),%xmm0
而在64位中(ABI保证函数入口处的堆栈指针对齐),需要27个字节:
0000000000000000 : 0: 48 b8 f0 de bc 9a 78 56 34 12 movabs $0x123456789abcdef0,%rax a: 50 push %rax b: 48 b8 10 32 54 76 98 ba dc fe movabs $0xfedcba9876543210,%rax 15: 50 push %rax 16: 66 0f 6f 04 24 movdqa (%rsp),%xmm0
如果将任何一个与MOVLHPS
版本进行比较,您会注意到它是最长的:
0000000000000000 : 0: 48 b8 f0 de bc 9a 78 56 34 12 movabs $0x123456789abcdef0,%rax a: 66 48 0f 6e c0 movq %rax,%xmm0 f: 48 b8 10 32 54 76 98 ba dc fe movabs $0xfedcba9876543210,%rax 19: 66 48 0f 6e c8 movq %rax,%xmm1 1e: 0f 16 c1 movlhps %xmm1,%xmm0
长度为33个字节。
直接从指令内存加载的另一个优点是movdqa
不依赖于任何先前的内容。很可能,@Paul R给出的第一版是最快的版本。pcmpeqw xmm0,xmm0
/ psrld xmm0, 31
来生成一个由四个32位整数1
值组成的向量),或者将立即数移动到寄存器movq
并使用pshufd
广播。 - Peter Cordes