Isi*_*ows 5 cpu-architecture cpu-registers
我见过的大多数支持本机标量硬件FP的体系结构都将它们推到了一个与主寄存器组分开的完全独立的寄存器空间中。
我见过的大多数支持本机标量硬件FP的体系结构都将它们推到了一个与主寄存器组分开的完全独立的寄存器空间中。
st(0)通过st(7)索引的每个项目。这可能是最流行的区别。它只能通过加载/存储到内存或将比较结果发送到EFLAGS与其他寄存器进行交互。(286 fnstsw ax和i686 fcomi)。addsd xmm1, m64或subsd xmm1, m64,但你只能加载和存储,通过寄存器movq xmm1, r/m64,movq r/m64, xmm1和朋友。这与ARM64 NEON相似,尽管它与ARM的标准标量指令集略有不同。相反,许多矢量化指令甚至都不会为这种区别而烦恼,只是在标量和矢量之间作了区分。对于x86,ARM和MIPS,全部三个:
但是我想知道:是否有CPU体系结构将相同的寄存器空间重用于整数和浮点运算?
如果不是这样(由于兼容性之外的原因),是什么会阻止硬件设计人员选择走这条路?
当然,从历史上看,FPU 是 CPU 的可选部分(因此存在带/不带 FPU 的芯片版本)。或者它可以是可选的单独芯片(例如 8086 + 8087 / 80286 + 80287 / ...),因此 FPU 拥有自己的单独寄存器非常有意义。
当您制作纯整数版本的 CPU 时,您需要省略 FPU 寄存器文件以及 FP 执行单元(以及将结果写回 FP 寄存器的转发网络和逻辑)。
因此,历史上一直有使用单独的 FP 寄存器的先例。
但对于蓝天的全新设计来说,这是一个有趣的问题。如果您打算拥有 FPU,则必须对其进行集成,以便在进行 FP 比较等分支时获得良好的性能。double从软件和硬件的角度来看, 为 64 位整数 / 共享相同的寄存器是完全合理的。
然而,某种类型的 SIMD 对于现代高性能 CPU 也是必需的。CPU-SIMD(与 GPU 风格相反)通常使用短的固定宽度向量寄存器来完成,通常为 16 字节宽,但最近英特尔已将宽度扩大到 32 或 64 字节。仅使用 64 位标量整数寄存器的低 8 个字节会浪费大量空间(并且在以整数代码读取/写入它们时可能会产生功耗)。
当然,在 GP 整数和 SIMD 向量寄存器之间移动数据需要消耗指令,并且如果值得硬件成本的话,在整数和 SIMD 之间共享寄存器组会很好。
最好的情况是假设一个带有标量 FPU 的全新 ISA,特别是如果它只是一个 FPU 并且没有整数 SIMD。 即使在这种不太可能的情况下,仍然有一些原因:
采用单独架构寄存器的一个重要原因是指令编码空间/位。
对于为每个操作数选择 16 个寄存器的指令,每个操作数需要 4 位。您愿意拥有 16 个 FP和16 个整数寄存器,还是总共 16 个相互竞争变量寄存器分配的寄存器?
FP 密集型代码通常需要至少一些整数寄存器来用于指向数组的指针和循环控制,因此拥有单独的整数寄存器并不意味着它们在 FP 循环中全部“浪费”。
即,对于相同的指令编码格式,选择是在N个整数和N个FP寄存器与N个灵活寄存器之间,而不是在2N个灵活寄存器之间。因此,通过将它们拆分为 FP 和 int,您可以获得两倍的独立寄存器总数。
不过,32 个灵活的寄存器对于很多代码来说可能就足够了,而且许多真正的 ISA 确实有 32 个架构寄存器(AArch64、MIPS、RISC-V、POWER和许多其他 RISC)。每条指令需要 10 或 15 位(每条指令 2 或 3 个操作数,如add dst, src或add dst, src1, src2)。不过,只有 16 个灵活寄存器肯定比每个都有 16 个更糟糕。在对函数使用多项式近似的算法中,您通常需要在寄存器中包含大量 FP 常量,而这不会留下太多用于展开来隐藏 FP 指令延迟的常数。
摘要:对于软件来说,32 个组合/灵活的寄存器通常比 16 int + 16 fp 更好,但这需要额外的指令位。16 个灵活的寄存器会明显比 16 int + 16 FP 差,在某些 FP 代码中会遇到更严重的寄存器压力。
中断处理程序通常必须保存所有整数寄存器,但内核代码通常仅使用整数指令构建。因此,如果中断处理程序必须保存/恢复 32 个组合寄存器的完整宽度,而不是仅仅 16 个整数寄存器,那么中断延迟会更糟。他们可能仍然能够跳过 FPU 控制/状态寄存器的保存/恢复。
(中断处理程序只需要保存它实际修改的寄存器,或者如果调用C,则调用破坏的寄存器。但是像Linux这样的操作系统倾向于在进入内核时保存所有整数寄存器,因此它具有保存的线程状态一次用于处理ptrace修改另一个进程/线程状态的系统调用。至少它在系统调用入口点执行此操作;关于中断处理程序的 IDK。)
如果我们谈论 32int + 32fp 与 32 个灵活寄存器,并且组合的寄存器仅适用于标量double或float,那么这个论点并不真正适用。
说到调用约定,当您使用任何 FP 寄存器时,您往往会使用很多寄存器,通常是在没有非内联函数调用的循环中。拥有大量被调用破坏的 FP 寄存器是有意义的。
但对于整数,您往往希望调用破坏与调用保留的均匀混合,这样您就可以在小函数中使用一些暂存寄存器,而无需保存/恢复某些内容,而且当您需要时也可以使用大量寄存器来保存内容频繁调用函数。
不过,拥有一组寄存器可以简化调用约定。 为什么不将函数参数存储在 XMM 向量寄存器中?讨论了更多关于调用约定权衡的内容(过多的调用破坏与过多的调用保留。)不过,如果只有一个平面寄存器空间,则有关 XMM 寄存器中整数的内容将不适用。
这是另一组主要原因。
首先,我假设具有大型物理寄存器文件的高性能乱序设计,架构寄存器被重命名为. (另请参阅我的回答Why does mulss take only 3cycles on Haswell, different from Agner's instructions table? (Unrolling FP Loops with multiple accumators))。
正如 @PaulClayton 的回答所指出的,将物理寄存器文件拆分为整数和 FP 减少了每个寄存器文件中对读/写端口的需求。您可以提供 3 源 FMA 指令,而不必提供任何 3 输入整数指令。
(Intel Haswell 就是一个例子:adc和cmovcc仍然是 2 uop,但 FMA 是 1。Broadwell 也将 adc 和 cmov 变成了单 uop 指令。目前还不清楚寄存器读取是否是这个运行 7 unfused- 的循环中的瓶颈。 Skylake 上每个时钟域 uops,但 Haswell 上仅为 6.25。当将某些指令从只写目标更改为读+写,并添加索引寻址模式(blsi ebx, [rdi]到add ebx, [rdi+r8])时,速度会变慢。后一个版本每时钟运行约 5.7 次寄存器读取Haswell 上的时钟,或 Skylake 上的 ~7.08,与快速版本相同,表明 Skylake 可能在每个时钟 ~7 寄存器读取上遇到瓶颈。现代 x86 微架构非常复杂,并且有很多事情要做,所以我们不能真正做到这一点。由此得出很多结论,特别是因为最大 FP uop 吞吐量几乎与最大整数 uop 吞吐量一样高。)
然而,Haswell/Skylake 运行 4x 没有问题add reg, reg,每个时钟读取 8 个寄存器并写入 4 个。前面的示例构造为主要读取未写入的“冷”寄存器,但重复 4xadd将仅读取 4 个冷寄存器(或 1 次冷调节 4 次)作为来源。鉴于寄存器有限,目的地最多仅在几个周期前写入,因此可能会被旁路转发。
我不知道 Agner Fog 博客上的示例中的瓶颈到底在哪里,但似乎不太可能只是整数寄存器读取。可能也与尝试最大化未融合域微指令有关。
芯片上的物理距离是另一个主要因素:您希望将 FP 寄存器文件物理放置在 FP 执行单元附近,以减少获取操作数时的功耗和光速延迟。FP 寄存器文件具有较大的条目(假设为 SIMD),因此减少所需的端口数量可以节省访问这么多数据位的空间或功耗。)
将 FP 执行单元保留在 CPU 的一部分中可以使 FP 操作之间的转发比 FP->integer 更快。(绕过延迟)。x86 CPU 使 SIMD/FP 和整数保持紧密耦合,在标量和 FP 之间传输数据的成本较低。但一些 ARM CPU 基本上会停止 FP->int 的管道,所以我猜通常它们的交互更加松散。作为硬件设计的一般规则,两个小型快速设备通常比一个大型快速设备更便宜/功耗更低。
Agner Fog关于理想可扩展指令集的提案(现在在Github 上,称为 ForwardCom)引发了一些关于如何设计 ISA 的非常有趣的讨论,包括这个问题。
他最初的建议是建立一套统一的r0..r31架构寄存器,每个寄存器都是 128 位,支持高达 64 位(可选 128 位)的整数,以及单/双(可选四)FP。也可用作谓词寄存器(而不是具有标志)。它们还可以用作 SIMD 向量,可选硬件支持大于 128 位的向量,因此可以编写/编译软件以在将来自动利用更宽的向量。
出于上述原因,评论者建议将向量寄存器与标量寄存器分开。
具体来说,Hubert Lamontagne 评论道:
寄存器:
据我所知,单独的寄存器文件是好的。原因是,当您向寄存器文件添加更多读取和写入端口时,其大小会呈二次方增长(或更糟)。这使得 cpu 组件变得更大,从而增加了传播时间,增加了扇出,并增加了寄存器重命名器的复杂性。如果您为浮点操作数提供自己的寄存器文件,那么除了加载/存储、比较和转换操作之外,FPU 永远不必与内核的其余部分交互。因此,对于相同数量的 IPC,例如每个周期 2 个整数 2 个浮点,分离浮点操作意味着您将摆脱一个巨大的 8 读 4 写寄存器文件和重命名机制,其中整数 ALU 和 FP ALU 都必须连接到各处,到 2 期整数单元和 2 期 FPU。FPU 可以有自己的寄存器重命名单元、自己的调度程序、自己的寄存器文件、自己的写回单元、自己的计算延迟,并且 FPU ALU 可以直接连接到寄存器,并且整个 FPU 可以位于不同的部分芯片的。前端可以简单地识别哪些操作是 FPU 并将它们在那里排队。这同样适用于 SIMD。
进一步的讨论表明,将标量浮点与向量浮点分开是愚蠢的,SIMD int 和 FP 应该保持在一起,但专用标量整数本身确实有意义,因为分支和索引是特殊的。(即与当前的 x86 完全相同,除了标量整数之外的所有内容都在 XMM/YMM/ZMM 寄存器中完成。)
我想这就是阿格纳最终做出的决定。
如果您只考虑标量 float 和标量 int,则更需要统一架构寄存器,但出于硬件设计原因,将它们分开是很有意义的。
如果您对为什么 ISA 是这样设计的,以及如果我们有一个干净的记录会更好,如果您有足够的背景来理解所提出的观点,我强烈建议您阅读整个讨论主题。
Motorola 88100 有一个用于浮点和整数值的寄存器文件(31 个 32 位条目加上一个硬连线的零寄存器)。对于 32 位寄存器并支持双精度,必须使用寄存器对来提供值,这极大地限制了可以保存在寄存器中的双精度值的数量。
后续 88110 添加了 32 个 80 位扩展寄存器,用于附加(和更大)浮点值。
参与摩托罗拉 88k 开发的 Mitch Alsup 开发了他自己的加载存储 ISA(至少部分出于教学原因),如果我没记错的话,它使用统一的寄存器文件。
还应该注意的是,Power ISA(PowerPC 的后代)定义了一个“嵌入式浮点工具”,它使用 GPR 来表示浮点值。这降低了核心实现成本和上下文切换开销。
单独寄存器文件的一个好处是它提供了显式的存储,以减少直接有限超标量设计中的寄存器端口数(例如,为每个文件提供三个读取端口将允许所有对一个 FP,甚至三个源操作数 FMADD,以及一个基于 GPR 的操作并行启动,许多常见的基于 GPR 的操作对与具有单个寄存器文件的五个读端口相比,以支持 FMADD 和另一个双源操作)。另一个因素是容量是额外的,宽度是独立的;这既有优点也有缺点。此外,通过将存储与操作耦合,可以以更直接的方式实现高度不同的协处理器。考虑到芯片尺寸的限制,这对于早期的微处理器来说更为重要,
统一的寄存器文件有一些调用约定的优点;无论值的类型如何,值都可以在相同的寄存器中传递。统一的寄存器文件还允许所有寄存器用于所有操作,从而减少了不可用的资源。