【问题标题】:Assembly: Why are we bothering with registers?组装:我们为什么要为寄存器烦恼?
【发布时间】:2011-01-22 14:11:33
【问题描述】:

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

如果寄存器也可以在内存上工作,为什么我们还要费心只对寄存器进行算术运算?

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

片段 1

.data
    var dd 00000400h

.code

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


片段 2

.code

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



据我所知,大多数文本和教程主要在寄存器上进行算术运算。使用寄存器会更快吗?

编辑:那很快 :)

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

【问题讨论】:

  • 我不是专家(因此仅发表评论),但由于寄存器是 CPU 内部的,因此比内存操作更快。速度不是仅仅 1000 倍的小幅度,而是在很大程度上更快。
  • 一些(通常是 RISC)架构没有具有直接操作内存地址内容的算术指令,因为实际上它们很少使用。 (当然,x86 拥有它们,以及所有其他可能的奇怪特性,因为它是疯狂的旧 x86。)

标签: performance assembly cpu-architecture cpu-registers


【解决方案1】:

如果您查看计算机架构,您会发现一系列内存级别。那些靠近 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 没有任何板载缓存,而且我还没有老!如今,HDD 带有板载缓存,并且 Internet 缓存在任意数量的位置:内存中、HDD 上,还可能在缓存代理服务器上。

在远离 CPU 的每一步中,带宽延迟都会急剧下降(通常是数量级)。例如,一个 HDD 可能能够以 100MB/s 的速度读取,延迟为 5ms(这些数字可能不完全正确),而您的主内存可以以 6.4GB/s 的速度读取,延迟为 9ns(六个数量级震级!)。延迟是一个非常重要的因素,因为您不希望 CPU 等待的时间超过它必须等待的时间(对于具有深管道的架构尤其如此,但这是另一天的讨论)。

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

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

【讨论】:

  • +1 包括互联网。真正使存储层次结构完整。
【解决方案2】:

寄存器速度更快,而且您可以直接在内存上执行的操作也受到更多限制。

【讨论】:

  • 正确!此外,虽然“最终”值会被移回主内存,但只要事情发生在寄存器中,总线就可用于其他 [并行] 功能,例如提前读取到缓存等
  • 而且寄存器-寄存器指令更短,因此更快。他们不必计算有效地址。
  • 能否详细说明?
【解决方案3】:

x86 与您可能学习汇编的几乎所有其他“普通”CPU 一样,是一台注册机1。还有其他方法可以设计一些你可以编程的东西(例如,沿着内存中的逻辑“磁带”移动的图灵机,或生命游戏),但注册机已被证明基本上是实现高效率的唯一方法。性能。

https://www.realworldtech.com/architecture-basics/2/ 涵盖了可能的替代方案,例如现在也已过时的累加器或堆栈机器。尽管它省略了像 x86 这样的 CISC,它可以是加载存储或寄存器内存。 x86 指令can actually be reg,mem;注册,注册;甚至内存,注册。 (或有直接来源。)

脚注 1:称为register machine 的抽象计算模型不区分寄存器和内存;它所说的寄存器更像是真实计算机中的内存。我在这里说“寄存器机器”是指具有多个通用寄存器的机器,而不是只有一个累加器,或堆栈机器或其他任何东西。大多数 x86 指令有 2 个显式操作数 (but it varies),其中一个可以是内存。即使是像 6502 这样只能在一个累加器寄存器中真正进行数学运算的微控制器,几乎总是有一些其他寄存器(例如用于指针或索引),不像真正的玩具 ISA,如 Marie 或 LMC,编程效率极低,因为您需要继续存储和将不同的东西重新加载到累加器中,甚至不能在任何可以直接使用的地方保留数组索引或循环计数器。


由于 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,你不能用一个insn获取Nth寄存器的内容。)

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

拥有固定数量的寄存器是 CPU 可以通过register-renaming 进行乱序执行的一部分。当指令被解码时,寄存器号立即可用也使这更容易:永远不会读取或写入未知的寄存器。

请参阅Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables? (Unrolling FP loops with multiple accumulators) 以获取有关寄存器重命名的说明和一个具体示例(稍后对问题的编辑/我的答案的后面部分显示了使用多个累加器展开以隐藏 FMA 延迟的加速,即使它重用了相同的架构重复注册)。


带有存储转发的存储缓冲区基本上可以为您提供“内存重命名”。存储/重新加载到内存位置独立于之前的存储,并从该内核中加载到该位置。 (Can a speculatively executed CPU branch contain opcodes that access RAM?)

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

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

【讨论】:

  • 次要 nitpick:“kilo binary bytes”或“kibi bytes”的缩写形式是“KiB”,大写为 K。
  • @ecm 真的吗?这对我来说看起来很傻/很奇怪,但wikip 确认你是正确的。谢谢。啊,显然有一些只使用大写字母 K 的历史(在 Ki 前缀和荒谬的“kibi”发音之前)。 en.wikipedia.org/wiki/Binary_prefix#Main_memory
【解决方案4】:

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

【讨论】:

    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

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

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

      【讨论】:

        【解决方案7】:

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

        【讨论】:

          【解决方案8】:

          是的 - 您通常也可以轻松地推送/弹出寄存器以调用过程、处理中断等

          【讨论】:

            【解决方案9】:

            只是指令集不允许你做这么复杂的操作:

            add [0x40001234],[0x40002234]
            

            你必须通过寄存器。

            【讨论】:

            • 有很多 CPU 架构都允许这些指令。问题是速度,而不是允许的操作。之所以会出现有限的操作,是因为没有人会在他们的头脑中将它们从 RAM 到 RAM。
            • 问题是使用 IA32 指令集。而在 IA32 中,它不存在。你就是做不到。
            猜你喜欢
            • 2010-10-07
            • 1970-01-01
            • 1970-01-01
            • 2022-10-14
            • 2018-04-17
            • 2021-11-25
            • 2012-12-10
            • 2020-03-27
            • 1970-01-01
            相关资源
            最近更新 更多