x64调用约定使用的寄存器最多为前4个参数(rcx
、rdx
、r8
、r9
),并传递堆栈上的其余参数。在这种情况下,处理asm
过程中的补充参数的明显方法如下:
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;
但这里有一个巨大的问题:如果涉及内存操作,Intel CPU中的XCHG
指令具有隐含的LOCK
,这也意味着巨大的性能损失;也就是说,在最坏的情况下,总线将被锁定数百个时钟周期。(顺便说一句,我不能真正理解这个隐含的LOCK
是否具有真正可用的和智能的互锁指令,如XADD
、CMPXCHG
、BTS/BTR
等;如果我需要线程同步,naked XCHG
将是我的最后一个选择。)那么,如果我希望在寄存器中使用/保存/恢复params 5和params 6,我应该在这里做什么?是否有一种方法可以防止XCHG
指令的总线锁定?通常,这种情况下广泛使用的标准方法是什么?
2条答案
按热度按时间l2osamch1#
正如Ross的回答所解释的,标准的广泛使用的方法是溢出(然后重新加载)其他内容来释放tmp寄存器。
如果你把所有的东西都先加载到寄存器中,而不是根据需要加载,那就是搬起石头砸自己的脚。有时候你甚至可以使用一个arg作为内存源操作数,而根本不需要单独的
mov
加载。但为了回答标题问题
尽管有这个问题的标题,但我对swapping 2 registers in 8086 assembly language(16 bits)的回答确实有效地解决了寄存器与内存的交换问题,避免了
xchg
,因为lock
前缀是隐式的。溢出(然后重新加载)一个tmp reg,或者在最坏的情况下,在reg和mem之间进行XOR交换。这是 * 可怕的 *,基本上说明了为什么你的整个方法会导致效率低下的实现。(As Ross说,你可能还不能比编译器更高效地编写asm。一旦你了解如何创建高效的asm(Agner Fog的优化指南和微架构指南:https://agner.org/optimize/和https://stackoverflow.com/tags/x86/info中的其他链接),并且可以在优化的编译器输出中发现实际的低效率,那么如果您愿意,您 * 有时可以 * 手动编写更好的asm。(通常以编译器输出作为起点)。但通常您只会利用这些经验来调整您的C源代码,以便在可能的情况下从编译器中获得更好的asm。因为从长远来看,它更有用/更便于移植,而且它很少重要到值得手写asm。
此时,您更有可能通过查看
gcc -O3
的输出来学习如何创建更高效的asm。但是,错过优化的情况并不罕见,如果您发现了一些,您可能会在GCC的bugzilla中报告它们。)xchg
的隐式-lock
语义来自386。lock
前缀从8086年就存在了,用于add/or/and/etc [mem], reg or immediate
之类的指令。包括lock xchg
、它显然没有隐含的lock
行为(即使没有前缀)直到386。或者可能直到那时才有文档?IDK为什么英特尔做出了这样的改变。也许是为了原始的SMP 386系统。**您提到的其他指令是后来添加的:**386中的
bts
/btr
/btc
(但并非仅用于共享内存,因此隐式的lock
没有意义)。xadd
出现在486,而cmpxchg
直到Pentium才出现(486有一个未公开的cmpxchg
操作码,参见an old version of the NASM appendix A的注解)。这些CPU的设计时间晚于386,大概是在对原始SMP系统有了一些初步经验之后。正如你所说,英特尔明智地选择了 * 不 * 使
lock
隐式用于这些新指令,即使主要用例是用于多线程代码中的原子操作。SMP x86机器开始成为486和奔腾的一种东西,但UP机器上线程之间的同步不需要lock
。这是一个与x86 CMPXCHG是原子的相反的问题。如果是,为什么需要锁定?8086是单处理器机器,因此对于软件线程之间的同步,普通
add [mem], reg
相对于中断和上下文切换已经是原子的。(而且不可能同时执行多个线程)。文档中提到的遗留#LOCK
外部信号只对DMA观察者有影响,或者将MMIO连接到设备上的I/O寄存器(而不是连接到普通DRAM)。(On对于现代CPU,未跨缓存行边界拆分得可缓存内存上得
xchg [mem], reg
只执行一个缓存锁,确保缓存行从加载阅读L1 d到提交到L1 d得存储期间保持MESI独占或已修改状态.)我不知道为什么8086架构师(主要是莫尔斯设计的指令集)选择不使非原子的
xchg
具有可用的内存。(更正一下,我想他做到了,只是386改变了它;这个答案最初是在我知道这是一个386变化之前写的。)也许在8086上,让CPU在执行存储+加载事务的同时Assert#LOCK
并不慢多少?但是,对于x86的其余部分,我们却被这些语义所困。x86设计很少有非常前瞻性的思考,并且如果xchg
的主要用例是用于原子I/O,则它节省了代码大小以使lock
隐式化。无法禁用
xchg [mem], reg
中的隐式锁定您需要使用多个不同的指令。异或交换是可能的,但效率很低。可能还没有
xchg
那么差。取决于微体系结构和周围的代码(在执行任何后续加载之前,等待所有先前的存储执行并提交到L1 d缓存的时间有多长)。例如,某些运行中的缓存未命中存储可能会使其与内存相比成本非常高-目的地xor
,其可以将数据留在存储缓冲器中。编译器基本上从不使用
xchg
,即使在寄存器之间也是如此(因为it's not cheaper than 3mov
instructions on Intel,所以它通常不是一个有用的窥视孔优化),它们只使用它来实现seq_cst
存储顺序的std::atomic
存储(因为在大多数uarch上,它比mov
+mfence
更有效:Why does a std::atomic store with sequential consistency use XCHG?),并实现std::atomic::exchange
.但notstd::swap
带有reg或内存。如果x86有一个非原子的2或3 uop
swap reg,mem
,它偶尔会有用,但它没有。没有这样的指令。但是特别是x86-64有16个寄存器,你遇到这个问题只是因为你自己创建了它,给自己留一些暂存寄存器用于计算。
gt0wga4j2#
只要做编译器所做的事情。当你需要的时候,把参数从栈中加载到寄存器中,当需要释放寄存器的时候,把寄存器溢出到栈中它们自己的位置。这是一个标准的和广泛使用的方法,如果不是很优雅的话,用于处理需要比可用寄存器更多的问题。
另请注意,Windows x64调用约定要求“非易失性”(被调用方保存的)寄存器必须仅保存在序言中。(尽管您可以使用链接的展开信息在一个函数中拥有多个“序言”。)
因此,假设您需要使用所有被调用方保存的寄存器,并严格遵循Windows x64调用约定,则需要执行以下操作:
现在,希望你问自己是否真的需要做这些,答案是肯定和否定的。没有什么可以简化汇编代码的。如果你不关心异常处理,你就不需要unwind info指令,但是如果你想让你的代码像编译器一样高效,同时保持相对容易维护,你仍然需要其他的东西。
但是有一种方法可以避免所有这些,那就是使用C/C编译器。现在对汇编的需求并不多。你不可能写得比编译器更快,你可以使用内部函数来访问你想使用的任何特殊汇编指令。编译器可以考虑堆栈上的东西,它可以很好地完成寄存器分配。最小化所需的寄存器节省和溢出量。
(Microsoft C/C编译器甚至可以生成我前面提到的链接展开信息,以便仅在必要时才保存被调用方保存的寄存器。)