CPU 内有 xmm
寄存器,可以存储 128 位,表示一个向量。可以存储 4 个 32 位标量,又或者 2 个 64 位标量。SSE 指令可以做到一条指令就可以将向量中的每个元素都执行某个操作。元素可以是单精度、双精度和整数。SSE 指令集也可以只使用寄存器的低 32 或 64 位进行操作。一般 SSE 指令是普通指令加下面的后缀:
汇编指令后缀 | 精度 | 标量/向量 |
---|---|---|
ss |
单精度 | 标量 |
sd |
双精度 | 标量 |
ps |
单精度 | 向量 |
pd |
双精度 | 向量 |
比如:mulsd %xmm0, %xmm1
、movsd (%rcx, %rsi, 8), %xmm1
如果想操作整数向量,那么需要加一个 p
前缀,然后后缀可选 b
(8 位)、w
(16 位)、d
(32 位)、q
(64 位),如:paddq
。
如果想操作整数标量的话,直接使用朴素的指令集即可。
AVX 指令集可以选择 ymm
寄存器,相当于是 xmm
寄存器的扩展,有 256 位,也就是比如 %ymm0
的低 128 位是 %xmm0
。AVX 的指令只需要在 SSE 指令的基础上在最前面加一个 v
前缀,如:vpaddq
。
zmm
寄存器)。现代 CPU 不一定按照指令原来的顺序执行指令,因为我们考虑如果两个指令之间没什么数据关系的话,先执行谁都是无所谓的。不过我们会存在如下的数据冒险情况:
其中后两者数据冒险在传统的流水线架构中不会发生,但是在乱序执行的过程中,前一条指令先执行还是后一条指令先执行就会带来不同的影响。
现代处理器将借助 Renaming Table 、Reorder Buffer(ROB)和 Reservation Station(RS)使得乱序执行正常进行。
处理器内部其实有很多物理寄存器。而对于一个逻辑寄存器(指令中使用的,如 %rax
这些)并不是直接对应于一个物理寄存器的。物理寄存器通常比逻辑寄存器多的多,逻辑寄存器到物理寄存器的映射关系也不是固定不变的,而是可以根据 Renaming Table 动态调整的。
然后考虑如下的例子:
CPU 取出一个指令之后,将会将指令加入 ROB 和 RS 中。在 ROB 中,每个指令都会有一个 tag 进行标识。我们假定上图中指令 1 和指令 2 正在执行,执行的结果逻辑上应该是分别存放到 %xmm0
和 %xmm2
中,但 ROB 还记录其实际被存放到物理寄存器 7 和 2 中。而由于现在还没执行完毕,我们在 Renaming Table 中记录一下后续的 %xmm0
和 %xmm2
将分别来自指令 1 和指令 2 的结果。
再取出指令 3 后,将在 ROB 中记录其需要的操作数,也就是 %xmm0
和 %xmm1
,在 Renaming Table 中被映射为 t1
和 t2
。指令也被送往 RS,但是指令并不会立刻发射,因为指令 1 和 2 没有执行完毕,其所依赖的操作数没有准备就绪。这条指令只能停留在 RS 里等待发射。RS 还没开始执行,所以我们也不去考虑其运算结果存放到哪个物理寄存器中。由于指令 3 会更新 %xmm2
寄存器,所以相应的修改 Renaming Table 中 %xmm2
指向指令 3 的运算结果。
然后是取出第四条指令:
此时指令 1 执行完毕了,结果已经存到了物理寄存器 7 中,所以可以把所有的 t1
替换成 Preg7
:
此时我们发现,指令 4 的所有操作数都已经被放入一个物理寄存器中了,这说明操作数已经准备就绪,指令 4 可以开始执行。指令 4 将会从 RS 中发射到相应的计算单元。我们发现,指令 3 还没准备好执行,指令 4 可以先于指令 3 执行,体现出乱序执行。
当指令 4 开始执行前,我们可以从物理寄存器的空闲列表中分配一个物理寄存器给指令 4 ,表示指令 4 的运算结果应该放入这个物理寄存器中。
传统的流水线寄存器,一个时钟周期只能发射一个指令。而对于现代寄存器,我们可以考虑,如果 RS 中多个指令的操作数都准备就绪了,那么可以直接全部发射出去,这就实现了一个周期发射多条指令。
同时,现代 CPU 往往有多个运算单元,同一种类的运算单元也可能有多个,因此可以同时处理多个发射出去的指令。