从处理器中获取内存

Memory Fetching from processor

假设我得到以下汇编代码:

多 b,b,b

表示先计算b的平方,然后存入b。我的问题是: 当处理器尝试从 b(b 的值)获取内存时,考虑到它正在尝试获取同一个变量,会尝试两次还是只尝试一次?

3 操作数内存到内存机器仅存在于理论计算机科学 AFAIK 中。如今,每个人都在构建寄存器机或(在低端微控制器中)累加器机(通常带有一个或两个指针寄存器以及实际的累加器),因为拥有寄存器比需要内存(或缓存)更有效store/reload 计算链中每一步的往返。

但是,是的,可以(也是一个好主意)设计一个 CPU 以在多个源操作数编码相同地址时仅执行一次缓存读取来尽可能优化。

I need to find the program size in bytes. So I was just wondering if the value of b will be accessed twice?

这两件事是不相关的。机器代码仍然需要编码 b 两次,除非有一条特殊的 "square" 指令只能容纳一个源操作数。在那种情况下,您肯定希望它只被访问一次。 (它可能没有单独的助记符,只是 mul 的不同操作码,当两个源操作数相同时汇编程序可以使用它)。

或者机器编码让第二个源操作数显式引用第一个源操作数,而不必再次独立指定 b 的地址。但是 CPU 可以将 b, same_as_first 解码为 b, b 然后读取 b 两次。即只在解码器中处理该特殊情况,而不是在该情况的操作数读取阶段提供优化路径。花费额外的晶体管来实现优化可能是值得的,但你不能假设任何事情。 (即使在这种特殊情况下,指令编码对第二个操作数使用 "ditto" 编码。)顺便说一句,我完全是在编造它;我还没有听说过像这样的真正的 ISA。 VAX 对两个操作数都有完全灵活的编码,两者都可以是内存,但 AFAIK 它们不能相互引用。


英特尔 P6 系列确实对寄存器读取(而不是内存读取)进行了优化,这很重要,因为它从其永久/退休寄存器文件中读取端口有限。

x86 是一种寄存器架构,主要包含 2 个操作数指令。大多数指令支持内存源或内存目标(但不是在一条指令中同时支持)。但没关系,这里有趣的类比是 P6 如何处理读取寄存器源操作数,这类似于您对 3 操作数内存到内存架构中的源操作数感到疑惑。

Intel P6 微架构是一个 3-wide 乱序设计,带有寄存器重命名。大多数 "simple" x86 指令解码为单个内部 uop,这就是它在乱序内核中实际重命名和跟踪的内容。 (Pentium Pro / Pentium II 是最初的 P6 微架构。后来的 P6 家族成员,Pentium III 和 Pentium M 是 3-wide,而 Core2 和 Nehalem 是 4-wide。)

Sandybridge is a new microarchitecture family 切换到使用物理寄存器文件,不再有寄存器读取停顿。


P6-family 有一个永久寄存器文件,用于保存架构寄存器的退役状态。但是乱序机制将寄存器输入值保存在 ReOrder Buffer 中。 (与具有物理寄存器文件的设计不同,其中 ROB 具有指向 PRF 条目的 指针 ,而不是直接的值)。

如果一个uop的寄存器输入来自一个还没有退役的uop,那么ROB中的值仍然是"live"。这是正常情况:大多数代码用新值重复重写相同的寄存器,特别是因为 32 位 x86 只有 8 个整数寄存器。大多数 x86 指令都是带有 read/write 目的地的 2 操作数,例如 add edx, ecx。 (edx += ecx).

但是当重命名一组输入来自最近未被写入的寄存器的微指令时(即写入该寄存器的微指令已经退休),ROB 读取阶段(重命名阶段之后)必须从永久寄存器文件中将所有需要的 "cold" 寄存器值读入 ROB。

See Agner Fog's microarch PDF,章节:Pentium Pro/PII/PIII 流水线,6.5 ROB 部分,阅读了解更多详情。 在这些第一代 P6 CPUs 中,永久寄存器文件只有 2 个读取端口,但是 3 uops 最多 2 个输入,每个最多可以读取 6 个寄存器。如果它们都是冷的,则该问题组的 ROB 读取阶段总共需要 3 个周期。 但是如果同一个冷寄存器被读取6次,没有问题:硬件通知重叠,只读取一次。

更多示例:如果 rdxrcx 最近没有被写入,lea rax, [rdx + rcx*4] 将消耗 2 个读取端口(因此值在重新排序缓冲区)。但是 lea rax, [rdx + rdx*4] 只会占用 1 个端口。

我以 LEA 为例,通过单独的只写目标更像 RISC。但是(寄存器读取停顿的)性能问题是相同的:add 必须读取两个源寄存器。

在同一组 3 或 4 微指令中 renamed/issued 的其他指令(实际上是微指令)也可以共享读取端口,如果它们中的任何一个读取相同的 "cold" 寄存器。例如add eax, esi/add edx, esi同组重命名只需要读取esi一次。 (eax 对于第一个 add 也可能是冷的,但是第二个 add 将第一个 eax 刚刚写入的 eax 作为其输入。ROB-read 阶段显然还不能读取值,所以它只是标记第一个 add uop 将其结果写入第二个 add 或类似的输入字段。)

当然,写入 eax 会使它在重新排序缓冲区中 "live" 直到指令退出,这就是为什么 P6 通常可以 运行 快速即使只有几个读取用于最近未写入的寄存器的端口。 P6 是在 x86-64 出现之前设计的(Core2 是第一个支持 64 位的 P6 成员,Nehalem 引入了更多的寄存器读取带宽)。在 x86-64 中拥有更多寄存器可以在寄存器中保留更多常量,因此您更有可能读取最近未写入的寄存器。

Sandybridge 切换到物理寄存器文件,这允许 ROB 增长,因为每个条目都更加紧凑:不需要每个值的副本作为每个 uop 的输入,多个 uops 读取同一个寄存器指向相同的 PRF 条目。 Sandybridge 还添加了 AVX,它将矢量寄存器扩展到 256 位。在每个 uop 条目中为两个 256b 输入留出空间真是太疯狂了。