【问题标题】:How to move 128-bit immediates to XMM registers如何将 128 位立即数移动到 XMM 寄存器
【发布时间】:2011-10-03 00:05:13
【问题描述】:

上面已经有a question,但它被关闭为“模棱两可”,所以我要打开一个新的 - 我找到了答案,也许它也会对其他人有所帮助。

问题是:如何编写一系列汇编代码来初始化一个具有 128 位立即数(常数)值的 XMM 寄存器?

【问题讨论】:

    标签: assembly x86 sse simd


    【解决方案1】:

    只是想补充一点,可以在 Agner Fog 的手册 Optimizing subroutines in assembly language,生成常量,第 13.8 节,第 124 页中阅读有关使用汇编生成各种常量的信息。

    【讨论】:

    • 谢谢,忘了那个:)。顺便说一句,这本书建议 SHUFPD 可行,但在这种情况下,我认为我的 MOVLHPS 建议更好(至少更短)
    【解决方案2】:

    您可以这样做,只需一条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 需要执行的几个额外的微指令,对于一个无法通过几次移位的全一生成的任意常数。

    如果更简单,您可以将常量放在 jit 编译的函数之前或之后,而不是放在单独的部分中。但由于 CPU 已分离 L1d / L1i 高速缓存和 TLB,因此通常最好将常量组合在一起,与指令分开。

    如果常量的两半相同,则可以使用 SSE3 广播加载它
    movddup (m64), %xmm0

    【讨论】:

    • 没错,但我是动态生成代码,添加代码比添加内存部分更简单:)(顺便说一句,你的示例应该使用 .align 16,对吧?)
    • @Virgil:不同版本的 gcc 工具链在这一点上有些不一致,但通常 .align 指令采用 2 的幂参数,所以 .align 4 表示对齐 2 的倍数^ 4 = 16 个字节。
    • 您将如何在 x86-32 上执行此操作?我不知道如何翻译 pc 相对寻址。
    • @JanusTroelsen 你试过 (%eip) - 用 'e' 而不是 'r'。
    • .p2align 4 将是一个不错的选择。它总是意味着 2 的幂对齐,并被引入以阻止 .align 的疯狂,这意味着不同汇编器(或同一汇编器的版本?)上的不同事物。我认为它比 SSE 存在的时间更长,因此推荐它应该是安全的。
    【解决方案3】:

    作为 10000 种方法之一,使用 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 记录在哪里?我在任何英特尔说明手册中都找不到该说明。
    • :错误:“pinsrq”的操作数类型不匹配
    • movq 指令不允许通用寄存器作为第二个操作数。所以这“更快”只是因为它不能很快地组装起来。从好的方面来说,pinsrq 技巧很有效。
    • @DavidWohlferd:movq 有两种形式:您可能会想到MOVQ xmm1, xmm2/m64,它可以在 32 位或 64 位模式下组装。但这当然是使用MOVQ xmm, r/m64 形式,即REX+MOVD,仅在64位模式下可用。显然有些汇编器仍然称它为movd,所以如果它不能汇编,试试movd xmm0, rax。或者更好的是,使用movdqa 加载一个常量。
    【解决方案4】:

    最好的解决方案(特别是如果您想坚持使用 SSE2 - 即避免使用 AVX)用立即值的两个 64 位一半初始化两个寄存器(例如 xmm0 和 xmm1),请执行 MOVLHPS xmm0,xm​​m1 为了初始化一个 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?
    • 我的意思是 CPID 功能标志。 SSE3/4 对你帮助不大。我想我找到了一种更简单的方法来使用 AVX 指令,但我忽略了它,因为支持它的 CPU 并不普遍。
    • @Virgil:Paul 的正确:SSE4.1 的PINSRQ xmm0, rax, 1 可以替换movq / movlhps。此外,您应该说 RAX,而不仅仅是 AX。 AX 特指RAX 的低16 位。您可以称它为 A,但这只是令人困惑。无论如何,这比仅使用加载指令加载它更糟糕。
    • 另外,对于要与整数指令一起使用的值,punpcklqdq xmm0, xmm1 可能是比movlhps 更好的选择。对于常量,显然无序执行可以隐藏从 FP shuffle 到整数指令的旁路延迟(在重要的 CPU 上),但这并没有什么坏处。无论如何,我认为在大多数代码中,最好从.rodata 部分加载一个常量,而不是将其嵌入到指令流中。通常 uop-cache 空间很有价值,前端吞吐量也是如此。单个movdqa 要快得多,除非它在缓存中丢失。但如果经常运行它就不会
    【解决方案5】:

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

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

    因此,虽然没有办法立即加载XMM 寄存器,但可以从“紧挨着”存储的值到代码执行的地方。这会产生类似的东西:

    .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-relative 内存访问,仍然可以在 24 字节内完成此操作(假设堆栈指针在入口处对齐;否则,需要未对齐的加载):

    00000000 :
       0: 68 78 56 34 12 推 $0x12345678
       5: 68 f0 de bc 9a push $0x9abcdef0
       答:68 98 ca db fe push $0xfedbca98
       f: 68 10 32 54 76 推 $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 推 %rax
       b: 48 b8 10 32 54 76 98 ba dc fe movabs $0xfedcba9876543210,%rax
      15:50 推%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相对,它清晰且非常简短。另一方面,它对内存的一个可能“昂贵”的命中(与应该始终在缓存中的代码相反。)
    • 写。要缓存,通过从与加载它的代码相同的缓存行中的地址加载常量,你很有可能它是缓存热的 - 因为执行代码必须在它运行时被提取,并且至少L2 是统一的,对于常量的负载,它可能不会比 L2 缓存命中开销更差。
    • @AlexisWilke:相比而言,uop 缓存很小,而且非常珍贵。通常不值得在 insn 流中嵌入 128b 常量。动态生成简单的值可能值得(例如,pcmpeqw xmm0,xmm0 / psrld xmm0, 31 生成四个 32 位整数 1 值的向量),或者可能将立即数移动到寄存器 movq,并使用pshufd.
    猜你喜欢
    • 2012-01-30
    • 2011-01-14
    • 2017-10-16
    • 1970-01-01
    • 2014-01-16
    • 2019-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多