如何将128位立即数移动到XMM寄存器

24

已经有一个问题与此相关,但由于“含糊不清”而关闭,因此我开了一个新的问题 - 我找到了答案,也许它会帮助其他人。

问题是:如何编写一系列汇编代码以使用128位立即值(常量)初始化XMM寄存器?


只是想补充一下,大家可以在Agner Fog的手册《汇编语言优化子程序》中阅读关于使用汇编生成各种常量的内容。具体可参考第13.8节“生成常量”,第124页。 - Norbert P.
5个回答

19

谢谢,我忘了那个 :). 顺便说一下,书上建议使用SHUFPD,虽然可以工作,但在这种情况下,我认为我的提议MOVLHPS更好(至少更短)。 - Virgil

11

你可以像这样做,只需使用一个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


1
没错,但我是动态生成代码的,添加代码比添加内存部分更简单 :) (顺便说一句,你的示例应该使用.align 16,对吧?) - Virgil
2
@Virgil: gcc工具链的不同版本在这方面有些不一致,但通常.align指令需要一个2的幂次方参数,因此.align 4表示对其到2 ^ 4 = 16字节的倍数。 - Paul R
1
你会如何在x86-32上完成这个操作?我无法弄清如何翻译PC相对寻址。 - Janus Troelsen
1
@JanusTroelsen 你试过用 'e' 而不是 'r' 来执行 (%eip) 吗? - Alexis Wilke
1
.p2align 4 是一个不错的选择。它总是表示2的幂对齐,并且被引入是为了防止在不同的汇编器(或同一汇编器的不同版本)中,.align 表示不同的含义。我认为它已经存在比 SSE 更长时间了,因此应该安全地推荐使用它。 - Peter Cordes
显示剩余4条评论

9

作为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.
错误:`pinsrq'的操作数类型不匹配。 - thang
movq 指令不允许将通用寄存器作为第二个操作数。因此它只是在无法快速汇编时才会更“快”。好的一面是, pinsrq 技巧可以使用。 - David Wohlferd
1
@DavidWohlferd:movq有两种形式:您可能在想MOVQ xmm1,xmm2/m64,它可以在32位或64位模式下汇编。但是这当然是使用MOVQ xmm,r/m64形式,它是REX+MOVD,仅在64位模式下可用。显然,一些汇编器仍将其称为movd,因此如果无法汇编,请尝试movd xmm0,rax。或者更好的方法是使用movdqa加载常量。 - Peter Cordes

6

最佳解决方案(特别是如果您想坚持使用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

关于SSE2和AVX的部分似乎是一个“不合逻辑”的论点 - 也许你的意思是SSE3 / SSSE3 / SSE4而不是AVX? - Paul R
我指的是CPID特性标志。SSE3/4对你帮助不大。我觉得我找到了一个更简单的方法,可以使用AVX指令来实现,但是由于支持它的CPU并不普遍,所以我忽略了它。 - Virgil
1
@Virgil:Paul是正确的:SSE4.1的PINSRQ xmm0,rax,1可以替换movq/movlhps。此外,你应该说RAX,而不仅仅是AX。AX具体指RAX的低16位。你可以称之为A,但那只会让人感到困惑。无论如何,这比使用加载指令加载要糟糕。 - Peter Cordes
此外,对于要与整数指令一起使用的值,“punpcklqdq xmm0,xmm1”可能比“movlhps”更好。 对于常量,显然乱序执行可以隐藏FP洗牌到整数指令的旁路延迟(在那些有影响的CPU上),但这并不会损害性能。 无论如何,我认为在大多数代码中,最好只从“.rodata”部分加载常量,而不是将其嵌入到指令流中。 通常,uop-cache空间很有价值,前端吞吐量也很重要。 一个单独的“movdqa”速度要快得多,除非它在缓存中丢失。 但如果这经常运行,则不会丢失。 - Peter Cordes

6

在指令流中嵌入常量有多种方法:

  1. 使用立即操作数
  2. 从PC相对地址加载

因此,虽然没有办法将常量直接加载到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给出的第一版是最快的版本。

做得很好,展示了每一个可能性并显示了哪一个是最短的。个人而言,我更喜欢IP相对地址,它清晰而且非常简短。另一方面,它可能会对内存造成一次“昂贵”的访问(与应始终在缓存中的代码相反)。 - Alexis Wilke
关于缓存,通过从与加载它的代码在同一缓存行内的地址加载常量,您有很大的机会使其成为缓存热点 - 因为执行代码必须在运行时被获取,至少L2是统一的,因此加载常量的开销可能不会超过L2缓存命中开销。 - FrankH.
1
@AlexisWilke:与其它缓存相比,uop缓存很小且很珍贵。通常不值得在insn流中嵌入128b常量。可以通过即时生成简单的常量(例如pcmpeqw xmm0,xmm0 / psrld xmm0, 31来生成一个由四个32位整数1值组成的向量),或者将立即数移动到寄存器movq并使用pshufd广播。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接