3 操作数内存到内存机器仅存在于理论计算机科学 AFAIK 中。如今,每个人都在构建寄存器机器,或者(在低端微控制器中)累加器机器(通常带有一个或两个指针寄存器以及实际的累加器),因为拥有寄存器比需要内存(或缓存)更有效为计算链中的每一步存储/重新加载往返。
但是,是的,当多个源操作数编码相同的地址时,通过只进行一次缓存读取来设计一个 CPU 来优化是可能的(也是一个好主意)。
我需要找到程序大小(以字节为单位)。所以我只是想知道 b 的值是否会被访问两次?
这两件事是无关的。机器代码仍然必须对b 进行两次编码,除非有一条特殊的“方形”指令只能容纳一个源操作数。在这种情况下,您肯定希望它只被访问一次。 (它可能没有单独的助记符,只是mul 的不同操作码,当两个源操作数相同时汇编器可以使用该操作码)。
或者机器编码可能让第二个源操作数显式引用第一个源操作数,而不是必须再次独立指定b 的地址。但是 CPU 可以将b, same_as_first 解码为b, b,然后读取b 两次。即仅在解码器中处理该特殊情况,而不是在操作数读取阶段为该情况提供优化路径。花费额外的晶体管来实现这种优化可能是值得的,但你不能假设任何事情。 (即使在这种特殊情况下,指令编码对第二个操作数也有“同上”编码。)顺便说一句,我完全是在编造这个;我还没有听说过像这样的真正的 ISA。 VAX 对两个操作数都有完全灵活的编码,两者都可以是内存,但 AFAIK 它们不能相互引用。
英特尔 P6 系列确实对寄存器读取(而不是内存读取)进行了这种优化,这很重要,因为它的永久/停用寄存器文件的读取端口有限。
x86 是一种寄存器架构,主要包含 2 操作数指令。大多数指令都支持内存源或内存目标(但不能同时在一条指令中)。但请不要介意,这里有趣的类比是 P6 如何处理读取寄存器源操作数,这与您想知道的 3 操作数内存到内存架构中的源操作数类似。
英特尔 P6 微架构是一个 3 宽的乱序设计,带有寄存器重命名。大多数“简单”x86 指令解码为单个内部 uop,这实际上是它在无序核心中重命名和跟踪的内容。 (Pentium Pro / Pentium II 是最初的 P6 微架构。P6 系列的后期成员 Pentium III 和 Pentium M 是 3 宽,而Core2 和 Nehalem 是 4 宽。)
Sandybridge is a new microarchitecture family 切换到使用物理寄存器文件,并且不再有寄存器读取停顿。
P6 系列有一个永久寄存器文件,用于保存架构寄存器的停用状态。但是乱序机制将寄存器输入值保存在 ReOrder Buffer 中。 (与具有物理寄存器文件的设计不同,其中 ROB 具有指向 PRF 条目的指针,而不是直接的值)。
如果 uop 的寄存器输入来自尚未退役的 uop,则该值在 ROB 中仍然“有效”。这是正常情况:大多数代码用新值重复重写相同的寄存器,特别是因为 32 位 x86 只有 8 个整数寄存器。大多数 x86 指令都是带有读/写目标的 2 操作数,例如 add edx, ecx。 (edx += ecx)。
但是,当重命名一组输入来自最近未写入的寄存器的微指令时(即写入该寄存器的微指令已退休),ROB 读取阶段(重命名阶段之后)必须读取所有需要从永久寄存器文件中将“冷”寄存器值放入 ROB。
See Agner Fog's microarch PDF,章节:Pentium Pro / PII / PIII 管道,6.5 ROB 阅读部分了解更多详情。 在这些第一代 P6 CPU 中,永久寄存器文件只有 2 个读取端口,但 3 个 uop,每个最多 2 个输入,总共可以读取多达 6 个寄存器。如果它们都是冷的,ROB 读取阶段将需要 3 个周期来处理该问题组。 但是如果同一个冷寄存器被读取6次,就没有问题:硬件通知重叠,只读取一次。
更多示例:如果最近没有写入 rdx 和 rcx,lea rax, [rdx + rcx*4] 将消耗 2 个读取端口(因此这些值在 ReOrder 缓冲区中还没有进行中)。但是lea rax, [rdx + rdx*4] 只会消耗 1 个端口。
我以 LEA 为例,使其更像 RISC,具有单独的只写目标。但无论哪种方式,性能问题(寄存器读取停顿)都是相同的:add 必须读取两个源寄存器。
如果它们中的任何一个读取相同的“冷”寄存器,则在同一组 3 或 4 个微指令中重命名/发出的其他指令(实际上是微指令)也可以共享读取端口。例如add eax, esi / add edx, esi 在同一组中被重命名只需读取一次esi。 (eax 对于第一个 add 可能也很冷,但第二个 add 将第一个刚刚写入的 eax 作为其输入。ROB 读取阶段显然还无法读取该值,所以它只是标记第一个add uop 将其结果写入第二个add 的输入字段,或类似的东西。)
当然,写入eax 会使其在重新排序缓冲区中“存活”,直到指令退出,这就是为什么即使只有几个读取端口用于未写入的寄存器,P6 也可以正常运行。 P6 是在 x86-64 出现之前设计的(Core2 是第一个支持 64 位的 P6 成员,Nehalem 引入了更多的寄存器读取带宽)。在 x86-64 中拥有更多寄存器可以在寄存器中保留更多常量,因此您更有可能读取最近未写入的寄存器。
Sandybridge 切换到物理寄存器文件,这允许 ROB 增长,因为每个条目都更加紧凑:不需要每个值的副本作为每个 uop 的输入,读取相同寄存器的多个 uop 指向相同PRF 条目。 Sandybridge 还添加了 AVX,它将向量寄存器扩大到 256 位。在每个 uop 条目中为两个 256b 输入留出空间会非常疯狂。