我正在编写一个JIT编译器,我很惊讶地发现,在Win64调用约定中,许多x86-64寄存器都是非易失性的(被调用者保留)。在我看来,非易失性寄存器只能在所有可以使用这些寄存器的函数中工作。在数值计算的情况下,尤其如此,您希望在叶函数中使用许多寄存器,比如某种高度优化的矩阵乘法。但是,例如,16个SSE寄存器中只有6个是易失性的,所以如果你需要使用更多的东西,你就会有很多事情要做。
所以是的,我没有得到它。这里的权衡是什么?
答案 0 :(得分:4)
如果寄存器是调用者保存,则调用者总是必须在函数调用周围保存或重新加载这些寄存器。但是如果寄存器是被调用者保存的,那么被调用者只需要保存它使用的寄存器,并且只有当它知道它们将被使用时(即可能在早期退出情况下根本不存在)。 这个约定的缺点是被调用者不知道调用者,所以它可能会保存已经死的寄存器,但我想这被认为是较小的关注。
答案 1 :(得分:0)
拥有nonvolatile
个注册表的优势是:效果。
移动的数据越少,CPU的效率就越高。
volatile
寄存器越多,CPU所需的能量就越多。
答案 2 :(得分:0)
只有6个调用被破坏的xmm寄存器的Windows x86-64调用约定不是一个很好的设计,你是对的。大多数SIMD(和许多标量FP)循环不包含任何函数调用,因此它们在保持调用的寄存器中获取数据没有任何好处。保存/恢复是纯粹的缺点,因为它的任何调用者都很少使用这种非易失性状态。
在x86-64 System V中,所有向量寄存器都是call-clobbered,这可能是另一种方式。保留1或2个调用保留在很多情况下会很好,特别是对于进行一些数学库函数调用的代码。 (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)实际上都有混合。 这减少了在呼叫者和被叫者中完成溢出/恢复的总工作量。
强制调用者在每个函数调用周围溢出/重新加载所有内容都不利于代码大小或性能。在函数的开头/结尾保存/恢复一些调用保留的regs允许非叶子函数在call
的寄存器中保留一些内容。
考虑一些计算一些事情的代码然后cout << "result: " << a << "foo" << b*c << '\n';
这是对std::ostream operator<<
的4个函数调用,并且它们通常不内联。保持cout
的地址和您刚刚在非易失性寄存器中计算的本地人意味着您只需要一些便宜的mov reg,reg
指令来设置下一次调用的args。 (或者在堆栈参数调用约定中push
。)
但是有一些可以在不保存的情况下使用的call-clobbered寄存器也非常重要。不需要所有架构寄存器的函数只能使用call-clobbered寄存器作为临时寄存器。这避免了将溢出/重新加载到调用者的依赖链(对于非常小的被调用者)的关键路径中,以及保存指令。
有时复杂的函数会保存/恢复一些调用保留的寄存器,只是为了获得更多的总寄存器(就像你在XMM中看到数字运算一样)。这通常是值得的;保存/恢复调用者的非易失性寄存器通常比将自己的局部变量溢出/重新加载到堆栈更好,特别是如果你不得不在任何循环内执行此操作。
call-clobbered寄存器的另一个原因是,在函数调用之后,通常你的一些值“死”:你只需要它们作为函数的args。在call-clobbered寄存器中计算它们意味着你不必保存/恢复任何东西来释放这些寄存器,而且你的被调用者也可以自由地使用它们。这在调用在寄存器中传递args的约定时更好:您可以直接在arg传递寄存器中计算输入。 (如果你在函数之后也需要它们,那么将它们复制到调用保存的regs或将它们溢出到堆栈内存中。)
(我喜欢调用保留与call-clobbered之类的术语,而不是调用者保存与被调用者保存。后面的术语暗示有人必须保存寄存器,而不仅仅是让死值死掉。不稳定/非 - 易失性也不错,但这些术语也有其他技术意义,如C关键字,或闪存与DRAM。)
答案 3 :(得分:0)
被调用者只需要保存/恢复被调用者保存的(非易失性,调用保留)寄存器,它需要暂时更改其值(其中一些可能不会被堆栈链/堆栈跟踪中的任何调用者使用,但是被调用者不知道这一点),并且调用者只需要保存/恢复调用者保存的(易失性,调用破坏)在调用后需要的寄存器(未来堆栈链中的被调用者可能实际上不会修改,但是来电者不知道这一点)。
通常,at least on Microsoft x64 calling convention,您会在堆栈上看到很多显式保存的非易失性寄存器,但没有显式保存易失性寄存器——我认为这个想法是编译器永远不会到达调用者需要显式保存的阶段在调用之前保存一个寄存器,特别是一个本身不是程序中变量的表达式;相反,它可以提前计划并完全避免使用这些寄存器,使用寄存器但不优化堆栈外的变量后备存储,将寄存器用于传递给被调用函数的参数,这些参数在调用被调用函数后已失效,因为它们不是t 定义为程序中的变量,或使用易失性寄存器。
被调用者在函数序言中显式地将它需要保持修改的任何非易失性寄存器推送到函数序言中的堆栈,并在尾声中恢复它们。它可以将它们保存在易失性寄存器中,但必须将它们恢复到非易失性寄存器或将它们保存到堆栈中(在这种情况下,保存/存储被称为溢出)如果被调用函数本身进行调用,并且它不能将它存储在另一个非易失性寄存器,因为这样也需要保存该寄存器。
我同意调用者保存意味着无论调用者是否使用寄存器都需要保存寄存器。这不是真的,即使它确实使用了寄存器,它甚至可能不必保存寄存器,因为它知道在调用之后不需要它,或者可能根本不会调用。
保持平衡是很好的。拥有一个而不是其他的只是一个缺点,但有时偏向于一种类型可能是最佳的,例如非易失性,该寄存器可能主要用于被调用者函数而不是调用者函数,就像彼得建议的 xmm
寄存器。
我认为拥有所有非易失性寄存器比拥有所有易失性寄存器伤害更大,因为您会保存调用后可能在调用者中死掉的参数(这就是参数是易失性的原因;此外,保留返回值寄存器是不可能的,因此您必须至少有一个易失性寄存器用于该寄存器或在堆栈上返回值,这会更慢),并且您也无法在不将值保存到堆栈的情况下暂时修改寄存器因为只有非易失性寄存器可用,而如果它们都是易失性寄存器,则您可以将值存储在寄存器中,直到进行调用或根本没有调用。总会有一个调用函数(除非它是基础框架),但是叶函数比基础框架多得多,基础框架必须不遵守调用约定,以优化非易失性寄存器的节省,如果严格遵守,可能不会优化它们,而调用约定中定义了不保存易失性寄存器的叶函数。
如果所有寄存器都是易失性的,这仍然是一个缺点,因为非易失性寄存器可以使您更容易编译自己的应用程序,因为负担在被调用函数上,它可能在一些单独编译的库中。此外,看到所有易失性寄存器在制作陷阱帧时被保存,而不是非易失性寄存器(this is the case on Microsoft x64 calling convention at least,除非有异常或上下文切换),如果所有寄存器都是易失性的,那么常规系统调用将有更多的时间/空间损失。