x86,就像你可能学习汇编的几乎所有其他“普通”CPU一样,是一个寄存器机器1。还有其他方法可以设计一些你可以编程的东西(例如,沿着内存中的逻辑“磁带”移动的图灵机,或生命游戏),但注册机已被证明基本上是实现高效率的唯一方法。表现。
https://www.realworldtech.com/architecture-basics/2/涵盖了可能的替代方案,例如现在也已过时的累加器或堆栈机器。尽管它省略了像 x86 这样的 CISC,它可以是加载存储或寄存器内存。x86 指令实际上可以是reg,mem;注册,注册;甚至内存,注册。(或有直接来源。)
脚注 1:称为寄存器机的抽象计算模型不区分寄存器和内存;它所说的寄存器更像是真实计算机中的内存。我在这里说“寄存器机器”是指具有多个通用寄存器的机器,而不是只有一个累加器,或堆栈机器或其他任何东西。大多数 x86 指令有 2 个显式操作数(但它会有所不同),最多其中之一可以是内存。即使是像 6502 这样只能在一个累加器寄存器中真正进行数学运算的微控制器,几乎总是有一些其他寄存器(例如用于指针或索引),这与像 Marie 或 LMC 这样的真正玩具 ISA 不同,它们的编程效率极低,因为您需要继续存储和将不同的东西重新加载到累加器中,甚至不能在任何可以直接使用的地方保留数组索引或循环计数器。
由于 x86 被设计为使用寄存器,因此您无法真正完全避免它们,即使您想要并且不关心性能。
当前的 x86 CPU 在每个时钟周期可以读取/写入比内存位置更多的寄存器。
例如,英特尔 Skylake 每个周期可以从/向其 32KiB 8 路关联 L1D 缓存执行两次加载和一次存储(最佳情况),但每个时钟可以读取多达 10 个寄存器,并写入 3 或 4 个(加上 EFLAGS)。
构建具有与寄存器文件一样多的读/写端口的 L1D 缓存将非常昂贵(在晶体管数量/面积和功耗方面),特别是如果您想保持它尽可能大。构建一个可以像 x86 使用具有相同性能的寄存器那样使用内存的东西在物理上可能是不可能的。
此外,写入一个寄存器然后再次读取它基本上具有零延迟,因为 CPU 检测到这一点并将结果直接从一个执行单元的输出转发到另一个执行单元的输入,绕过回写阶段。(参见https://en.wikipedia.org/wiki/Classic_RISC_pipeline#Solution_A._Bypassing)。
执行单元之间的这些结果转发连接称为“旁路网络”或“转发网络”,对于寄存器设计,CPU 执行此操作比所有内容都必须进入内存并退出的情况要容易得多。CPU 只需检查 3 到 5 位的寄存器编号,而不是 32 位或 64 位地址,即可检测需要立即将一条指令的输出作为另一操作的输入的情况。(并且这些寄存器号被硬编码到机器代码中,因此它们可以立即使用。)
正如其他人所提到的,使用 3 位或 4 位来寻址寄存器使机器代码格式比每条指令都具有绝对地址时更紧凑。
另请参阅https://en.wikipedia.org/wiki/Memory_hierarchy:您可以将寄存器视为与主内存分开的小型快速固定大小的内存空间,其中仅支持直接绝对寻址。(你不能“索引”一个寄存器:给定一个N
寄存器中的整数,你不能N
用一个 insn 得到第 th 寄存器的内容。)
寄存器对于单个 CPU 内核也是私有的,因此乱序执行可以随心所欲。有了内存,它必须担心其他 CPU 内核可以看到的顺序。
拥有固定数量的寄存器是让 CPU为无序执行进行寄存器重命名的一部分。当指令被解码时,寄存器号立即可用也使这更容易:永远不会读取或写入未知的寄存器。
请参阅为什么 mulss 在 Haswell 上只需要 3 个周期,与 Agner 的指令表不同?(使用多个累加器展开 FP 循环)解释寄存器重命名和一个具体示例(稍后对问题的编辑/我的答案的后面部分显示了使用多个累加器展开以隐藏 FMA 延迟的加速,即使它重用相同建筑登记反复)。
带有存储转发的存储缓冲区基本上可以为您提供“内存重命名”。存储/重新加载到内存位置独立于之前的存储,并从该内核中加载到该位置。(推测性执行的 CPU 分支是否可以包含访问 RAM 的操作码?)
使用堆栈参数调用约定的重复函数调用和/或通过引用返回值是相同字节的堆栈内存可以重复使用多次的情况。
即使第一个存储仍在等待其输入,秒存储/重新加载也可以执行。(我已经在 Skylake 上对此进行了测试,但如果我曾经在任何地方的答案中发布了结果,IDK。)