14

在一篇博客文章中读到,最近的 X86 微架构也能够在寄存器重命名器中处理常见的寄存器归零习惯用法(例如与自身异或寄存器);用作者的话来说:

“寄存器重命名器也知道如何执行这些指令——它可以将寄存器本身归零。”

有人知道这在实践中是如何工作的吗?我知道一些 ISA,如 MIPS,包含一个在硬件中始终设置为零的架构寄存器。这是否意味着在内部,X86 微体系结构在内部具有类似的“零”寄存器,这些寄存器在方便时映射到?或者我的心智模型对这些东西在微架构上的工作方式不太正确?

我问的原因是因为(从某些观察)看来mov,在一个循环中从一个包含零的寄存器到一个目的地,仍然比在循环内通过 xor 将寄存器归零快得多。

基本上发生的事情是我想根据条件将循环内的寄存器归零;这可以通过提前分配一个架构寄存器来存储零(%xmm3在这种情况下),在整个循环期间不会修改,并在其中执行以下操作来完成:

movapd  %xmm3, %xmm0

或者使用 xor 技巧:

xorpd   %xmm0, %xmm0

(两种 AT&T 语法)。

换句话说,选择是在循环之外提升一个常量零还是在每次迭代中重新实现它。后者将实时架构寄存器的数量减少了一个,并且,由于处理器对异或习语的假定特殊情况感知和处理,它似乎应该与前者一样快(特别是因为这些机器有更多的物理无论如何,寄存器都比架构寄存器要好,因此它应该能够在内部执行与我在程序集中所做的等效的操作,方法是在内部提升常量零甚至更好,同时完全了解和控制自己的资源)。但似乎并非如此,所以我很好奇是否有任何具有 CPU 架构知识的人可以解释是否有一个很好的理论理由。

本例中的寄存器恰好是 SSE 寄存器,而机器恰好是 Ivy Bridge;我不确定这两个因素的重要性。

4

3 回答 3

16

执行摘要xor ax, ax:与较慢的指令相比,每个周期最多可以运行四个mov immediate, reg指令。

详细信息和参考:

维基百科对寄存器重命名有一个很好的概述:http ://en.wikipedia.org/wiki/Register_renaming

Torbj¨orn Granlund 的 AMD 和 Intel x86 处理器的指令延迟和吞吐量时序位于:http://gmplib.org/~tege/x86-timing.pdf

Agner Fog 在他的微架构研究中很好地涵盖了细节:

8.8 寄存器分配和重命名

寄存器重命名由寄存器别名表 (RAT) 和重排序缓冲区 (ROB) 控制......来自解码器和堆栈引擎的微操作通过队列进入 RAT,然后进入 ROB 读取和保留站。RAT 每个时钟周期可以处理 4 µops。RAT 可以在每个时钟周期重命名四个寄存器,甚至可以在一个时钟周期内重命名同一个寄存器四次。

独立的特殊情况

将寄存器设置为零的常用方法是与自身进行异或运算或将其与自身相减,例如 XOR EAX,EAX。如果两个操作数寄存器相同,Sandy Bridge 处理器会识别出某些指令与寄存器的先前值无关。该寄存器在重命名阶段设置为零,不使用任何执行单元。这适用于以下所有指令:XOR、SUB、PXOR、XORPS、XORPD、VXORPS、VXORPD 以及 PSUBxxx 和 PCMPGTxx 的所有变体,但不适用于 PANDN 等。

不需要执行单元的指令

上述通过 XOR EAX、EAX 等指令将寄存器设置为零的特殊情况在寄存器重命名/分配阶段进行处理,而不使用任何执行单元。这使得这些归零指令的使用非常高效,每个时钟周期的吞吐量为四个归零指令。

于 2013-08-03T00:00:28.323 回答
10

归零中最大的性能成本隐藏在这句话中:

基本上发生的事情是我想根据条件将循环内的寄存器归零

这句话暗示了一个分支。即使正确预测了分支,它仍然可能比将寄存器归零的成本更高。

至于寄存器重命名...

在 OutOfOrder (OOO) CPU 中,每次您写入寄存器时,CPU 都会为您提供一个新寄存器。如果您执行了这三个指令:

xor eax,eax
add eax,eax
add eax,1

然后对于第一条指令,CPU(如果它是最近的英特尔 CPU)只是更新它的映射,说 eax 现在指的是内部零寄存器。在第一次添加时,它从 eax 读取(两次,因为它被用作输入两次),然后更新其映射以指向一个新寄存器并将结果写入该寄存器。第二次添加也会发生同样的事情。因此,在这三个指令的过程中,eax 寄存器被更改为指向三个不同的物理寄存器。

为什么?因为这:

mov eax,[esi]    ; Load from esi
add eax, 1
mov [esi], eax   ; Store to esi
mov eax,[esi+4]  ; Load from esi+4
add eax, 1
mov [esi+4], eax ; Store to esi+4

在 OOO 处理器上,对性能的主要限制之一是依赖关系。指令一到三必须按顺序执行。指令四到六必须按顺序执行。但是这两个块之间没有依赖关系。因此,一对三和四对六可以并行执行。但是,它们都指的是eax。

没问题。寄存器重命名解决了这个问题。第一条和第四条指令同时执行。CPU 为指令流中的每个点创建一个单独的 eax 映射,随后的指令对这些重命名的寄存器进行操作。这允许两个指令块完全并行执行。

由于各种原因,这实际上非常复杂,但它确实有效,它是让现代 CPU 运行得如此之快的主要因素之一。

无论如何,长话短说,“xor eax,eax”甚至永远不会执行,这很酷。这种优化可以应用于任何总是产生零或总是产生一的指令,或其他任何指令,但英特尔只会在重要时花费晶体管来执行此操作。我猜 xorpd 还没有成功。

我在博客上写了这个(http://randomascii.wordpress.com/2012/12/29/the-surprising-subtleties-of-zeroing-a-register/),因为我觉得它很酷。我也喜欢“add”和“sub”的想法,它们大多是相同的指令,由于这种行为,它们的性能可能会略有不同或有很大不同,尽管只是在从自身减去寄存器的情况下。

于 2015-03-05T01:12:22.840 回答
1

Ice Lake 客户端微架构

除了没有延迟之外,零惯用语的另一个好处是,在现代英特尔微架构上,重命名、移动消除和零惯用语阶段甚至发生在微指令的调度之前。因此,虽然零移动惯用语作为 uop 存在,但它不会竞争允许更多 ILP 的执行端口。

由于重命名器检测并删除了零习语,因此它们没有执行延迟。

于 2020-01-07T05:12:09.387 回答