首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >如何在最新的64位Intel CPU上交换堆栈顶部与没有隐式锁定的寄存器?

如何在最新的64位Intel CPU上交换堆栈顶部与没有隐式锁定的寄存器?
EN

Stack Overflow用户
提问于 2019-04-27 19:31:36
回答 2查看 207关注 0票数 0

x64调用约定使用最多前4个参数(rcxrdxr8r9)的寄存器,并将其余参数传递到堆栈上。在这种情况下,在asm过程中处理补充参数的明显方法如下:

代码语言:javascript
复制
procedure example(
  param1, //rcx
  param2, //rdx
  param3, //r8
  param4, //r9
  param5,
  param6
);
asm
  xchg param5, r14 // non-volatile registers, should be preserved
  xchg param6, r15 // non-volatile registers, should be preserved

  // ... procedure body, use r14–r15 for param5–param6

  mov r15, param6
  mov r14, param5  
end;

但这里有一个巨大的问题:如果涉及内存操作,英特尔CPU中的XCHG指令具有隐式LOCK,这也意味着巨大的性能损失;也就是说,在最坏的情况下,总线将锁定数百个时钟周期。(顺便说一句,我真的不能理解这种隐含的LOCK有真正可用的和智能的互锁指令,如XADDCMPXCHGBTS/BTR等;如果我需要线程同步,裸体XCHG将是我最后的选择。)那么,如果我希望在寄存器中/从寄存器中使用/保存/恢复params5和params6时使用简短而优雅的东西,我应该怎么做?是否有防止XCHG指令的总线锁定的黑客方法?一般来说,对于这种情况,标准的、广泛使用的方法是什么?

EN

回答 2

Stack Overflow用户

发布于 2019-04-28 04:42:05

正如罗斯的回答所解释的那样,标准的广泛使用的方法是溢出(然后重新加载)其他东西来释放临时注册表项。

首先将所有内容加载到寄存器中,而不是按需加载,这是在搬起石头砸自己的脚。有时,您甚至可以使用arg作为内存源操作数,而不需要单独的mov加载。

但要回答标题问题:

尽管有问题的标题,我在swapping 2 registers in 8086 assembly language(16 bits)上的回答确实有效地解决了寄存器与内存的交换问题,因为隐式的lock前缀而避免了xchg。溢出(稍后重新加载)一个tmp reg,或者在最坏的情况下,在reg和mem之间进行XOR交换。这太可怕了,基本上说明了为什么你的整个方法会导致低效的实现。

(正如Ross所说,您可能还没有能力编写比编译器更高效的asm。一旦您了解了如何创建高效的asm (Agner Fog的优化指南和microarch指南:https://agner.org/optimize/,以及https://stackoverflow.com/tags/x86/info中的其他链接),并且能够发现优化编译器输出中的实际低效之处,如果您愿意,有时可以手动编写更好的asm。(通常以编译器输出作为起点)。但通常情况下,如果可能的话,你只需要利用这种经验来调整你的C源代码,以便从你的编译器中获得更好的asm,因为从长远来看,这更有用/更可移植。而且它很少有足够的价值来手写asm。

在这一点上,您更有可能通过查看gcc -O3输出来学习制作更有效的asm的技术。但错过优化的情况并不少见,如果您发现了一些优化,您可能会在GCC的bugzilla上报告它们。)

implicit-**lock** xchg 的implicit-**lock**语义来自最初的8086。 lock前缀当时确实存在,以便与add/or/and/etc [mem], reg or immediate等指令一起使用。

你提到的其他指令是后来添加的:386年的bts/btr/btc,486年的,直到奔腾才添加的。(486有一个未记录的cmpxchg操作码,请参阅an old version of the NASM appendix A获取其注释)。

正如您所说,英特尔明智地选择不将lock隐含在这些新指令中,即使主要用例是用于多线程代码中的原子操作。随着486和奔腾的出现,SMP x86机器开始成为一种东西,但UP机器上的线程之间的同步不需要lock。这是一种与Is x86 CMPXCHG atomic, if so why does it need LOCK?相反的问题

8086是一台单处理器机器,因此对于软件线程之间的同步,普通的 add [mem], reg 对于中断已经是原子的,因此对于上下文切换。(同时执行多个线程是不可能的)。遗留的#LOCK外部信号仍然是文档中提到的唯一重要的wrt。DMA观察者,或MMIO到设备上的I/O寄存器(而不是普通DRAM)。

(在现代CPU上,未跨缓存行边界拆分的可缓存内存上的xchg [mem], reg只接受一个缓存锁,确保从读取L1d的加载到提交L1d的存储期间,该行一直处于MESI独占或已修改状态。)

我不知道为什么8086架构师(主要是Stephen Morse设计了指令集)选择不让非原子xchg具有可用的内存。也许在8086上,在执行存储+加载事务时,让CPU断言#LOCK的速度不是很慢吗?但是在x86的其余部分中,我们被这些语义所困住了。x86设计很少具有前瞻性,如果xchg的主要用例是原子I/O,那么它节省了代码大小,使lock隐式。

无法在xchg [mem], reg中禁用隐式锁

您需要使用多个不同的指令。xor交换是可能的,但效率非常低。不过,可能没有xchg那么糟糕,这取决于微体系结构和周围的代码(在执行任何后续加载之前,等待所有以前的存储执行并提交到L1d缓存会有多糟糕)。例如,一些运行中的高速缓存未命中存储可能使其与内存目的地xor相比非常昂贵,后者可能会将数据留在存储缓冲区中。

即使在寄存器之间,编译器基本上也不会使用xchg (因为it's not cheaper than 3 mov instructions on Intel,所以它通常不是一个有用的窥视优化)。他们只使用它来实现具有seq_cst内存顺序的std::atomic存储(因为在大多数uarches:Why does a std::atomic store with sequential consistency use XCHG?上,它比mov + mfence更有效),并实现std::atomic::exchange

如果x86有一个微码的但非原子的swap reg,mem,它有时会很有用,但它没有。

但特别是在x86-64有16个寄存器的情况下,您只会遇到这个问题,因为它是您自己创建的。为自己的计算留出一些临时规则。

票数 3
EN

Stack Overflow用户

发布于 2019-04-28 03:59:27

只要像编译器那样做就行了。在需要时,将参数从堆栈加载到寄存器中,根据需要将寄存器溢出到堆栈上它们自己的位置,从而释放寄存器。这是一种标准且被广泛使用的方法,用于处理所需寄存器多于可用寄存器的问题。

还要注意,Windows x64调用约定要求“非易失性”(被调用者保存的)寄存器只能保存在前言中。(尽管你可以使用链式展开信息在一个函数中有多个“前言”。)

因此,假设您需要使用所有被调用者保存的寄存器,并且严格遵循Windows x64调用约定,则需要如下所示:

代码语言:javascript
复制
example PROC    FRAME

_stack_alloc =  8   ; total stack allocation for local variables
                    ; must be MOD 16 = 8, so the stack is aligned properly;
_push_regs =    32  ; total size in bytes of the callee-saved registers
                    ; pushed on the stack

_param_adj =    _stack_alloc + _push_regs

; location of the parameters relative to RSP, including the incoming
; slots reserved for spilling parameters passed in registers

param1  =   _param_adj + 8h
param2  =   _param_adj + 10h
param3  =   _param_adj + 18h
param4  =   _param_adj + 20h
param5  =   _param_adj + 28h
param6  =   _param_adj + 30h

; location of local variables relative to RSP

temp1   =   0

    ; Save some of the callee-preserved registers
    push    rbp
    .PUSHREG rbp
    push    rbx
    .PUSHREG rbx
    push    rsi
    .PUSHREG rsi
    push    rdi
    .PUSHREG rdi

    ; Align stack and allocate space for temporary variables
    sub rsp, _stack_alloc
    .ALLOCSTACK 8

    ; Save what callee-preserved registers we can in the incoming
    ; stack slots reserved for arguments passed in registers under the
    ; assumption there's no need to save the later registers

    mov [rsp + param1], r12
    .SAVEREG r12, param1
    mov [rsp + param2], r13
    .SAVEREG r13, param2
    mov [rsp + param3], r14
    .SAVEREG r14, param3
    mov [rsp + param4], r15
    .SAVEREG r15, param4

    .ENDPROLOG

    ; ...

    ; lets say we need to access param5 and param6, but R14 
    ; is the only register available at the moment.  

    mov r14, [rsp + param5]
    mov [rsp + temp1], rax  ; spill RAX 
    mov rax, [rsp + param6]

    ; ...

    mov rax, [rsp + temp1]  ; restore RAX

    ; ...

    ; start of the "unofficial" prologue

    ; restore called-preserved registers that weren't pushed

    mov r12, [rsp + param1]
    mov r13, [rsp + param2]
    mov r14, [rsp + param3]
    mov r15, [rsp + param4]

    ; start of the "official" prologue
    ; instructions in this part are very constrained. 

    add rsp, _stack_alloc
    pop rdi
    pop rsi
    pop rbx
    pop rbp
    ret

example ENDP

现在,希望你会问自己,你是否真的需要做所有这些事情,答案是肯定的和否定的。在简化汇编代码方面,您可以做的并不多。如果你不关心异常处理,你就不需要unwind info指令,但如果你想让你的代码像编译器生成的代码一样高效,同时保持相对容易维护,你仍然需要几乎所有其他的东西。

但是有一种方法可以避免所有这些,只需使用C/C++编译器即可。这些天真的不需要太多的组装。您不太可能编写比编译器更快的代码,而且您可以使用内部函数来访问几乎任何您想要使用的特殊汇编指令。编译器可以担心数据在堆栈中的位置,它可以很好地完成寄存器分配,最大限度地减少必要的寄存器节省和溢出。

(Microsoft的C/C++编译器甚至可以生成我前面提到的链式展开信息,以便只有在必要时才能保存被调用者保存的寄存器。)

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/55880117

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档