汇编:为什么我们要困扰寄存器?

时间:2010-03-02 04:59:11

标签: performance math assembly cpu-architecture cpu-registers

我有一个关于装配的基本问题。

如果只能在寄存器上工作,我们为什么还要在寄存器上进行算术运算呢?

例如,以下两个原因(基本上)都要将相同的值计算为答案:

Snippet 1

.data
    var dd 00000400h

.code

    Start:
        add var,0000000Bh
        mov eax,var
        ;breakpoint: var = 00000B04
    End Start


Snippet 2

.code

    Start:
        mov eax,00000400h
        add eax,0000000bh
        ;breakpoint: eax = 0000040B
    End Start



 从我所看到的,大多数文本和教程主要在寄存器上进行算术运算。使用寄存器只是更快吗?

编辑:那很快:)

给出了一些很好的答案;根据第一个好的答案选择了最佳答案。

9 个答案:

答案 0 :(得分:25)

如果你看一下计算机架构,你会发现一系列内存级别。那些接近CPU的是快速,昂贵(每一点),因此很小,而在另一端你有大,慢和廉价的存储设备。在现代计算机中,这些通常类似于:

 CPU registers (slightly complicated, but in the order of 1KB per a core - there
                are different types of registers. You might have 16 64 bit
                general purpose registers plus a bunch of registers for special
                purposes)
 L1 cache (64KB per core)
 L2 cache (256KB per core)
 L3 cache (8MB)
 Main memory (8GB)
 HDD (1TB)
 The internet (big)

随着时间的推移,已经添加了越来越多的缓存级别 - 我记得CPU没有任何板载缓存的时候,我甚至都不老了!目前,硬盘驱动器带有板载缓存,互联网缓存在任意数量的地方:内存,硬盘驱动器,以及缓存代理服务器。

在远离CPU的每一步中,带宽会有显着(通常是数量级)的减少,并且延迟会增加。例如,HDD可能能够以100MB / s的速度读取,延迟为5ms(这些数字可能不完全正确),而主存储器的读取速度为6.4GB / s,延迟为9ns(6阶大小!)。延迟是一个非常重要的因素,因为您不希望CPU保持等待的时间超过其所需(对于具有深度管道的架构尤其如此,但这是另一天的讨论)。

这个想法是你经常会一遍又一遍地重复使用相同的数据,所以将它放在一个小的快速缓存中以便后续操作是有意义的。这被称为时间位置。局部性的另一个重要原则是空间局部性,它表示彼此靠近的存储位置可能几乎同时被读取。正是由于这个原因,从RAM读取将导致更大的RAM块被读取并放入CPU缓存中。如果它不适合这些局部性原则,那么内存中的任何位置都有可能同时被读取,因此无法预测接下来将要访问的内容以及所有级别的缓存在世界上不会提高速度。您可能只是使用硬盘驱动器,但我确定您知道在分页时计算机停止运行是什么样的(基本上使用HDD作为RAM的扩展)。除了硬盘驱动器(许多小型设备只有一个内存)之外,概念上可能没有内存,但与我们熟悉的相比,这将会非常缓慢。

拥有寄存器(只有少量寄存器)的另一个好处是它可以让你有更短的指令。如果您的指令包含两个(或更多)64位地址,那么您将获得一些长指令!

答案 1 :(得分:8)

寄存器更快,并且您可以直接在内存上执行的操作更加有限。

答案 2 :(得分:3)

寄存器的访问速度方式比RAM内存快,因为您不必访问“慢”内存总线!

答案 3 :(得分:2)

x86,像其他几乎一样"正常"您可能学习汇编的CPU是"register machine"。还有其他方法可以设计你可以编程的东西(例如图灵机沿着逻辑"磁带"在内存中移动),但注册机已经证明基本上是获得高性能的唯一方法

由于x86旨在使用寄存器,因此即使您想要并且不关心性能,也无法完全避免使用寄存器。

当前的x86 CPU每个时钟周期可以读取/写入比内存位置多得多的寄存器。

例如,英特尔Skylake每个周期可以对其32kiB 8路关联L1D缓存执行两次加载和一次存储(最佳情况),但可以read upwards of 10 registers per clock, and write 3 or 4 (plus EFLAGS)

使用与the register file一样多的读/写端口构建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。)

寄存器也是单个CPU内核专用的,因此乱序执行可以随心所欲地执行它们。对于内存,它必须担心其他CPU内核可以看到哪些顺序。

拥有固定数量的寄存器是CPU执行无序执行register-renaming的部分原因。在解码指令时立即获得寄存器编号也使这更容易:从来没有读取或写入尚未知的寄存器。

有关寄存器重命名的说明,请参阅Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables?,以及一个具体示例(我的答案的后续编辑部分显示了从展开多个累加器以隐藏FMA延迟的加速,即使它重复使用相同的建筑注册重复)。

具有存储转发的存储缓冲区基本上可以为您提供内存重命名"。存储/重新加载到内存位置与先前的存储无关,并从该核心内加载到该位置。

使用stack-args调用约定和/或通过引用返回值的重复函数调用是堆栈内存的相同字节可以多次重用的情况。

即使第一个商店仍在等待其输入,也可以执行秒存储/重新加载。 (我已经在Skylake上对此进行了测试,但如果我曾在任何地方将结果发布到答案中,我会对IDK进行测试。)

答案 4 :(得分:1)

我们使用寄存器因为它们很快。通常,它们以CPU的速度运行 寄存器和CPU缓存由不同的技术/结构和
制成 它们是昂贵的。另一方面,RAM便宜又慢100倍。

答案 5 :(得分:1)

一般来说,寄存器算法更快,更受欢迎。但是在某些情况下直接记忆算法很有用。 如果您只想在内存中增加一个数字(至少对于几百万条指令没有其他内容),那么单个直接内存算术指令通常比加载/添加/存储稍快。

此外,如果您正在执行复杂的数组操作,通常需要大量寄存器来跟踪您的位置以及数组的结束位置。在较旧的体系结构上,您可能会很快耗尽寄存器,因此可以选择在不切换任何当前寄存器的情况下将两位内存添加到一起非常有用。

答案 6 :(得分:0)

是的,使用寄存器要快得多。即使你只考虑从处理器到寄存器的物理距离与proc到内存的比较,你可以通过不发送电子到目前为止节省大量时间,这意味着你可以以更高的时钟速率运行。

答案 7 :(得分:0)

是的 - 通常你也可以轻松地推送/弹出寄存器来调用程序,处理中断等等。

答案 8 :(得分:-2)

只是指令集不允许你进行这样复杂的操作:

add [0x40001234],[0x40002234]

你必须通过寄存器。