处理器是否可以同时进行存储和算术运算?
Is processor can do memory and arithmetic operation at the same time?
在汇编和处理器的学习中,有一件事让我很兴奋,指令是如何完成的 :
add mem, 1
在我看来,处理器无法加载内存值 和 在同一条指令中处理算术运算。所以我认为它是这样发生的:
mov reg, mem
add reg, 1
mov mem, reg
如果我考虑具有 RISC Pipeline 的处理器,我们可以观察到一些停顿。像 i++
:
这样简单的指令令人惊讶
| Fetch | Decode | Exec | Memory | WriteB |
| Fetch | | | Decode | Exec | Memory | WriteB |
| Fetch | | | | Decode | Exec | Memory | WriteB |
(正如我在 Patterson 的书 Computer Architecture: A Quantative Approach 中读到的那样,寄存器在 Decode uOp 中读取,Store/Load 在 Memory uOp 中,我们允许自己在 Memory uOp 中获取寄存器的值。)
我说的对吗?或者现代处理器有特定的方法可以更有效地做到这一点?
没错,现代 x86 会将 add dword [mem], 1
解码为 3 微指令:加载、ALU 添加和存储。 (这实际上是对各种事物的简化,包括英特尔的微融合以及AMD如何始终在管道的某些部分将负载+ ALU保持在一起...)
这3个依赖操作不能同时发生,因为后面的必须等待前面的结果。
但是 独立 指令的执行可能会重叠,现代 CPU 非常积极地寻找并利用“指令级并行性”来 运行 您的代码每个时钟快于 1 uop。参见 this answer for an intro to what a single CPU core can do in parallel, with links to more stuff, like Agner Fog's x86 microarch guide, and David Kanter's write-ups of Sandybridge and Bulldozer。
但如果您查看 Intel 的 P6 和 Sandybridge 微体系结构系列,存储实际上是独立的存储地址和存储数据微指令。 store-address 微指令不依赖于加载或 ALU 微指令,可以随时将存储地址写入 。 (Intel的优化手册称之为Memory Order Buffer)。
为了提高前端吞吐量,存储地址和存储数据微指令可以解码为微融合对。对于 add
,load+alu 操作也可以,因此英特尔 CPU 可以将 add dword [rdi], 1
解码为 2 个融合域微指令。 (相同的 load+add micro-fusion 用于将 add eax, [rdi]
解码为单个 uop,因此任何“简单”解码器都可以解码它,而不仅仅是可以处理多 uop 指令的“复杂”解码器。这减少了前端瓶颈)。
这就是为什么 add [mem], 1
在 Intel CPU 上比 inc [mem]
更高效,即使 inc reg
与 [=18 一样高效(但更小) =]. (inc
无法对其 load+inc 进行微熔断,它设置的标志与 add
不同)。
但这只是帮助前端更快地将uops送入调度器;加载仍然必须 运行 与添加分开。
但是微熔断负载不必等待整个指令输入的其余部分就绪。考虑像 add [rdi], eax
这样的指令,其中 RDI 和 EAX 都是指令的输入,但是在 ALU 加 uop 之前不需要 EAX。一旦加载地址准备好并且有一个空闲的加载执行单元(AGU + 缓存访问),加载就可以执行。另见 .
registers are read in Decode uOp, Store/Load in Memory uOp and we allow ourselves to take the value of a register at the Memory uOp
所有当前的 x86 微体系结构都使用寄存器重命名的乱序执行(Tomasulo 算法)。指令被重命名并发布到核心的无序部分(ROB 和调度程序)。
直到一条指令从调度程序“分派”到执行单元时,才会读取物理寄存器文件。 (或者对于最近生成的输入,从其他 uops 转发。)
独立 指令可以重叠它们的执行。例如,Skylake CPU 可以维持每个时钟 4 个融合域/7 个非融合域微指令的吞吐量,包括 2 个加载 + 1 个存储,in a carefully crafted loop:
.loop: ; HSW: 1.12c / iter. SKL: 1.0001c
add edx, [rsp] ; 1 fused-domain uop: micro-fused load+add
mov [rax], edi : 1 fused-domain uop: micro-fused store-address+store-data
blsi ebx, [rdi] : 1 fused-domain uop: micro-fused load+bit-manip
dec ecx
jnz .loop ; 1 fused-domain uop: macro-fused dec+branch runs on port 6
Sandybridge 系列 CPUs 有一个 L1d 缓存,每个时钟能够进行 2 次读取 + 1 次写入。 (不过,在 Haswell 之前,只有 256 位向量可以绕过 AGU 吞吐量限制。参见 How can cache be that fast?。)
Sandybridge 系列的前端吞吐量是每个时钟 4 个融合域微指令,并且它们在后端有很多执行单元来处理各种指令混合。 (Haswell 和后来的有 4 个整数 ALU、2 个加载端口、一个存储数据端口和一个用于简单存储寻址模式的专用存储 AGU。因此它们通常可以在缓存未命中停止执行后快速“赶上”,快速乱序的房间 window 找更多工作要做。)
在汇编和处理器的学习中,有一件事让我很兴奋,指令是如何完成的 :
add mem, 1
在我看来,处理器无法加载内存值 和 在同一条指令中处理算术运算。所以我认为它是这样发生的:
mov reg, mem
add reg, 1
mov mem, reg
如果我考虑具有 RISC Pipeline 的处理器,我们可以观察到一些停顿。像 i++
:
| Fetch | Decode | Exec | Memory | WriteB |
| Fetch | | | Decode | Exec | Memory | WriteB |
| Fetch | | | | Decode | Exec | Memory | WriteB |
(正如我在 Patterson 的书 Computer Architecture: A Quantative Approach 中读到的那样,寄存器在 Decode uOp 中读取,Store/Load 在 Memory uOp 中,我们允许自己在 Memory uOp 中获取寄存器的值。)
我说的对吗?或者现代处理器有特定的方法可以更有效地做到这一点?
没错,现代 x86 会将 add dword [mem], 1
解码为 3 微指令:加载、ALU 添加和存储。 (这实际上是对各种事物的简化,包括英特尔的微融合以及AMD如何始终在管道的某些部分将负载+ ALU保持在一起...)
这3个依赖操作不能同时发生,因为后面的必须等待前面的结果。
但是 独立 指令的执行可能会重叠,现代 CPU 非常积极地寻找并利用“指令级并行性”来 运行 您的代码每个时钟快于 1 uop。参见 this answer for an intro to what a single CPU core can do in parallel, with links to more stuff, like Agner Fog's x86 microarch guide, and David Kanter's write-ups of Sandybridge and Bulldozer。
但如果您查看 Intel 的 P6 和 Sandybridge 微体系结构系列,存储实际上是独立的存储地址和存储数据微指令。 store-address 微指令不依赖于加载或 ALU 微指令,可以随时将存储地址写入
为了提高前端吞吐量,存储地址和存储数据微指令可以解码为微融合对。对于 add
,load+alu 操作也可以,因此英特尔 CPU 可以将 add dword [rdi], 1
解码为 2 个融合域微指令。 (相同的 load+add micro-fusion 用于将 add eax, [rdi]
解码为单个 uop,因此任何“简单”解码器都可以解码它,而不仅仅是可以处理多 uop 指令的“复杂”解码器。这减少了前端瓶颈)。
这就是为什么 add [mem], 1
在 Intel CPU 上比 inc [mem]
更高效,即使 inc reg
与 [=18 一样高效(但更小) =]. (inc
无法对其 load+inc 进行微熔断,它设置的标志与 add
不同)。
但这只是帮助前端更快地将uops送入调度器;加载仍然必须 运行 与添加分开。
但是微熔断负载不必等待整个指令输入的其余部分就绪。考虑像 add [rdi], eax
这样的指令,其中 RDI 和 EAX 都是指令的输入,但是在 ALU 加 uop 之前不需要 EAX。一旦加载地址准备好并且有一个空闲的加载执行单元(AGU + 缓存访问),加载就可以执行。另见
registers are read in Decode uOp, Store/Load in Memory uOp and we allow ourselves to take the value of a register at the Memory uOp
所有当前的 x86 微体系结构都使用寄存器重命名的乱序执行(Tomasulo 算法)。指令被重命名并发布到核心的无序部分(ROB 和调度程序)。
直到一条指令从调度程序“分派”到执行单元时,才会读取物理寄存器文件。 (或者对于最近生成的输入,从其他 uops 转发。)
独立 指令可以重叠它们的执行。例如,Skylake CPU 可以维持每个时钟 4 个融合域/7 个非融合域微指令的吞吐量,包括 2 个加载 + 1 个存储,in a carefully crafted loop:
.loop: ; HSW: 1.12c / iter. SKL: 1.0001c
add edx, [rsp] ; 1 fused-domain uop: micro-fused load+add
mov [rax], edi : 1 fused-domain uop: micro-fused store-address+store-data
blsi ebx, [rdi] : 1 fused-domain uop: micro-fused load+bit-manip
dec ecx
jnz .loop ; 1 fused-domain uop: macro-fused dec+branch runs on port 6
Sandybridge 系列 CPUs 有一个 L1d 缓存,每个时钟能够进行 2 次读取 + 1 次写入。 (不过,在 Haswell 之前,只有 256 位向量可以绕过 AGU 吞吐量限制。参见 How can cache be that fast?。)
Sandybridge 系列的前端吞吐量是每个时钟 4 个融合域微指令,并且它们在后端有很多执行单元来处理各种指令混合。 (Haswell 和后来的有 4 个整数 ALU、2 个加载端口、一个存储数据端口和一个用于简单存储寻址模式的专用存储 AGU。因此它们通常可以在缓存未命中停止执行后快速“赶上”,快速乱序的房间 window 找更多工作要做。)