assembly 在调用约定中使用非易失性寄存器有什么好处?

gdx19jrr  于 9个月前  发布在  其他
关注(0)|答案(5)|浏览(89)

我正在编写一个JIT编译器,我惊讶地发现,这么多的x86-64寄存器是非易失性的在Win 64调用约定中,非易失性寄存器(callee-preserved)。在我看来,非易失性寄存器只是在所有可能使用这些寄存器的函数中增加了更多的工作量。这在数值计算的情况下尤其如此,在这种情况下,您希望在叶函数中使用许多寄存器,比如说某种高度优化的矩阵乘法。然而,16个SSE寄存器中只有6个是易失的,所以如果你需要使用更多的寄存器,你会有很多溢出要做。
所以,我不明白。这是什么交易?

5gfr0r5j

5gfr0r5j1#

如果寄存器是调用者保存的,那么调用者 * 总是 * 必须在函数调用时保存或重新加载这些寄存器。但是如果寄存器是被调用者保存的,那么被调用者只需要保存它使用的寄存器,只有当它知道它们会被用来(即,在提前退出的情况下可能根本没有)。this 约定的缺点是被调用者不知道调用者,所以它可能会保存那些已经死了的寄存器,但我想这是一个较小的问题。

vu8f3i0k

vu8f3i0k2#

Windows x86-64调用约定只有6个call-clobbered xmm寄存器不是一个很好的设计,你是对的。大多数SIMD(和许多标量FP)循环不包含任何函数调用,所以它们从将数据保存在调用保留寄存器中没有任何好处。保存/恢复是纯粹的缺点,因为它很少比任何调用者使用这种非易失性状态。
在x86-64 System V中,所有的向量寄存器都是call-clobbered,这可能太远了。在许多情况下,保留1个或2个call-preserved是很好的,特别是对于进行一些数学库函数调用的代码。(Use gcc -fno-math-errno to let simple ones inline better;有时他们不这样做的唯一原因是他们需要在NaN上设置errno。)
how the x86-64 SysV calling convention was chosen:查看gcc编译SPECint/SPECfp的代码大小和指令数。
对于整数型的回调,每种都有一些肯定是好的,所有的“正常”调用约定(对于所有架构,不仅仅是x86)实际上都有混合。这减少了调用者和被调用者的溢出/恢复工作总量。
强制调用者在每个函数调用周围溢出/重新加载所有内容对代码大小或性能都没有好处。在函数的开始/结束处保存/恢复一些调用保留的寄存器可以让非叶函数在call s上保持一些东西在寄存器中。
考虑一些代码,计算一些事情,然后执行cout << "result: " << a << "foo" << b*c << '\n';,这是对std::ostream operator<<的4个函数调用,将cout的地址和刚刚计算的局部变量保存在非易失性寄存器中意味着您只需要一些廉价的mov reg,reg指令来设置下一次调用的args。(或者在stack-args调用约定中使用push)。
但是拥有一些可以不保存就使用的call-clobbered寄存器也非常重要。不需要所有架构寄存器的函数可以只使用call-clobbered寄存器作为临时寄存器。这避免了在调用者依赖链的关键路径中引入溢出/重载(对于非常小的被调用者),以及保存指令。
有时一个复杂的函数会保存/恢复一些调用保留的寄存器,只是为了获得更多的总寄存器(就像你在XMM中看到的数字运算)。这通常是值得的;保存/恢复调用者的非易失性寄存器通常比溢出/重新加载你自己的局部变量到堆栈更好,特别是如果你必须在任何循环中这样做的话。
使用call-clobbered寄存器的另一个原因是,通常在函数调用之后,有些值是“死”的:你只需要它们作为函数的参数。在call-clobbered寄存器中计算它们意味着你不必保存/恢复任何东西来释放这些寄存器,而且你的被调用者也可以自由地使用它们。这在通过寄存器传递参数的调用约定中更好:你可以直接在arg-passing寄存器中计算你的输入。(如果你在函数之后还需要它们,可以将任何输入复制到call-preserved寄存器或溢出到堆栈内存。)
(我喜欢call-preserved与call-clobbered这两个术语,而不是caller-saved与callee-saved。后一个术语意味着必须有人保存寄存器,而不是让死值死去。volatile /nonvolatile并不坏,但这些术语也有其他技术含义,如C关键字,或闪存与DRAM。

yrdbyhpb

yrdbyhpb3#

被叫方只需要保存/恢复保存的被叫方(非易失性,保留调用)寄存器,它需要随时更改(其中一些可能不会被堆栈链/堆栈跟踪中的任何调用者使用,但被调用者并不知道这一点),调用者只需保存/恢复调用者保存(volatile,call-clobbered)在调用后需要的注册(未来堆栈链中的被调用者可能不会实际修改,但调用者不知道这一点)。
通常情况下,至少在Microsoft x64调用约定中,您将在堆栈上看到许多显式保存的非易失性寄存器,但不是显式保存的易失性寄存器-我认为其想法是编译器永远不会到达调用者需要在调用之前显式保存寄存器的阶段,特别是表达式本身不是程序中的变量;相反,它可以提前计划并完全避免使用这些寄存器,使用寄存器但不优化堆栈外的变量备份存储,将寄存器用于传递给被调用方函数的参数,这些参数在调用被调用方函数后死亡,因为它们在程序中没有定义为变量,或者使用volatile寄存器。
被调用方在函数序言中对堆栈进行调用时,显式地推送它需要保持修改的任何非易失性寄存器,并在尾声中恢复它们。它可以将它们保存在易失性寄存器中,但必须将它们恢复到非易失性寄存器或将它们保存到堆栈(在这种情况下保存/存储被称为溢出)如果被调用者函数自身进行调用,并且它不能将其存储在另一个非易失性寄存器中,因为这样该寄存器也将需要被保存。
我同意caller saved意味着无论调用者是否使用它,都需要保存注册表。这是不正确的,即使它确实使用了注册表,也可能不必保存注册表,因为它知道在调用后它不需要它,或者根本不需要调用。
有一个均衡的平衡是好的。只有一个寄存器而没有另一个寄存器是一个缺点,但有时候偏向一种类型可能是最佳的,例如非易失性寄存器,这种寄存器可能主要用于被调用函数而不是调用函数,就像Peter建议的xmm寄存器一样。
我认为所有的非易失性寄存器比所有的易失性寄存器更有害,因为您保存的参数可能在调用后在调用者中失效(这就是参数不稳定的原因;此外,保留返回值寄存器是不可能的,所以你必须至少有一个volatile寄存器,或者在堆栈上返回值,这是较慢的),你也不能在不将值保存到堆栈的情况下暂时修改寄存器,因为只有非易失性寄存器可用,而如果它们都是易失性寄存器,你就可以在寄存器中存储值,直到进行调用或根本没有调用。(除非它是基本框架),但是存在比基本框架多得多的叶函数,并且基本框架将不得不遵守调用约定以便优化非易失性寄存器的保存,并且如果严格遵守它,则可能不会优化它们,而在调用约定中定义了不保存易失性寄存器的叶函数。
如果所有寄存器都是易失性的,这仍然是一个缺点,因为非易失性寄存器可以使编译您自己的应用程序更容易,因为负担在被调用函数上,而被调用函数可能在一些单独编译的库中。此外,在制作 trap frame 时,* 所有 * 易失性寄存器都被保存,而非非易失性寄存器(至少在Microsoft x64调用约定上是这样的,除非存在异常或上下文切换),如果所有寄存器都是易失性的,则对于常规系统调用将存在更多的时间/空间损失。

ncgqoxb0

ncgqoxb04#

您需要两者的混合,主要是因为只有在有非易失性寄存器可用的情况下才有可能进行某些优化。例如,假设您有一个全局常量,并且您希望在代码的性能关键部分的许多函数调用中重复访问其值。使用非易失性寄存器,您可以以RAM速度加载该值一次,然后以寄存器的速度反复访问它。如果没有这种优化,你只能以L1的速度访问这个值。也就是说,我绝对同意你的观点,即volatile寄存器总体上更有用,所以也许Windows的约定不是很平衡。

vlju58qv

vlju58qv5#

使用nonvolatile寄存器的优点是:性能
移动的数据越少,CPU的效率就越高。
volatile寄存器越多,CPU需要的能量就越多。

相关问题