10. 整数指令配对(PPlain 及 PMMX)
10.1 完美的配对
PPlain 及 PMMX有两条流水线来执行指令, 分别叫做 U-管道和V-管道。 在一定的条件下两条指令可以一个在 U-管道,一个在 V-管道 同时执行。 这可以使速度加倍。 因此将你的指令重新组织一下次序使它们配对是很有利的。
下面这些指令可以在任意的管道内配对:
- MOV 寄存器, 内存, 或是立即数到寄存器或内存
- PUSH 寄存器或立即数, POP 寄存器
- LEA, NOP
- INC, DEC, ADD, SUB, CMP, AND, OR, XOR,
- 还有一些形式的 TEST (见26.14章)
下面的指令只能在 U-管道 配对: ADC, SBB SHR, SAR, SHL, SAL 移动立即数位 ROR, ROL, RCR, RCL 移动立即数1位
下面的指令可以在任何管道运行,但是只能在 V-管道 配对:
near call short 及 near jump short 及 near 条件跳转。
除这些指令之外的整型指令都只能在 U-管道 运行, 而且不能配对。
两条连续的指令满足了下面的要求时就可以配对:
1. 第一条指令在 U 管道 中,第二条指令在 V 管道 中, 且它们都是可配对的。
2. 当第一条指令写一个寄存器的时候,第二条指令不去读/写它,例如:
MOV EAX, EBX / MOV ECX, EAX ; 写后面跟着读,不能配对 MOV EAX, 1 / MOV EAX, 2 ; 写后面跟着写,不能配对 MOV EBX, EAX / MOV EAX, 2 ; 读后面跟着写,可以配对 MOV EBX, EAX / MOV ECX, EAX ; 读后面跟着读,可以配对 MOV EBX, EAX / INC EAX ; 读后面跟着读写,可以配对
3. 在第2条规则里面, 寄存器的一部分作为整个寄存器来对待, 例如:
MOV AL, BL / MOV AH, 0; 写入相对寄存器的不同部分, 不能配对
4. 当两条指令同时写的是标志寄存器的不同部分时,规则2和3都可以忽略掉。 例如:
SHR EAX, 4 / INC EBX ; 可以配对
5. 一个写标记寄存器的指令可以和一个条件跳转配对, 而忽略掉规则 2 。 例如:
CMP EAX, 2 / JA LabelBigger ; 可以配对
6. 下面的指令对,虽然同时修改了栈指针,但是它们依然可以配对:
PUSH + PUSH, PUSH + CALL, POP + POP
7. 对于有前缀的配对指令有一些限制。 下面列出了几种形式的前缀:
- 用段前缀对非缺省段寻址的指令。
- 在 32 位代码中使用 16 位的数据, 或16位的代码中使用 32 位数据的带操作数尺寸前缀的指令。
- 16位模式中, 使用32位的基址寄存器或变址寄存器的带地址尺寸前缀的指令。
- 带重复前缀的字符串操作指令。
- 带LOCK前缀的锁定指令。
- 很多在 8086 处理器中没有实现的,有两个字节的操作码且其中第一个字节是 0FH的指令。
这个 0FH 字节的行为在 PPlain 上就像一个前缀, 但是后来的版本中就不是。 最常见的带 0FH 前缀的指令有: MOVZX, MOVSX, PUSH FS, POP FS, PUSH GS, POP GS, LFS, LGS, LSS, SETcc, BT, BTC, BTR,BTS,BSF,BSR, SHLD, SHRD,还有带两个操作数且没有立即数的 IMUL。
在 PPlain 上, 有前缀的指令除了近距离条件跳转外只能在 U 管道中执行。
PMMX 上, 带有操作数尺寸、地址尺寸或0FH前缀的指令可以在任意管道执行, 但是带有段前缀, 重复前缀, 或者锁定前缀的指令只能在 U 管道执行。
8. 既带有偏移量又带有立即操作数的指令在 PPlain 上不能配对, 而在 PMMX 上只能在 U 管道配对:
MOV DWORD PTR DS:[1000], 0 ; 不能配对, 或者只能在 U 管道配对 CMP BYTE PTR [EBX+8], 1 ; 不能配对, 或者只能在 U 管道配对 CMP BYTE PTR [EBX], 1 ; 可以配对 CMP BYTE PTR [EBX+8], AL ; 可以配对
关于既带有偏移量又带有立即操作数的指令在 PMMX 上配对的另一个问题是:这条指令的长度可能>=7字节, 这意味着, 一个时钟周期只有一条指令能被解码, 这些放在第12章解释。
9. 两条指令必须已经预读进来且被解码。 这些放在第 8 章解释。
10. PMMX 上, 对于 MMX 指令有特殊的配对规则:
- MMX 移位, pack 和 unpack 指令可以在任意的管道执行,但是不能跟另外一条 MMX 移位,pack 和 unpack 指令配对。
- MMX 乘法指令可以在任意管道运行,但是不能和另外一条 MMX 乘法指令配对。乘法指令需要消耗 3 个时钟周期,其中后两个时钟周期并行执行其它指令,就好象浮点指令那样 (参见第 24 章)。
- 一条访问内存或整型寄存器的 MMX 指令只能在 U 管道运行, 而且不能跟非 MMX 指令配对。
10.2 有缺陷配对
有几种情况下, 两条成对指令根本不能并行执行, 或者只是时间上部分重叠。 然而它们依然被当作是成对的, 因为第一条指令在 U 管道执行, 而第二条在 V 管道。而且随后的指令必须要在两条有缺陷配对的指令都完成后才开始运行。
有缺陷配对发生在以下条件下:
1. 如果第二条指令遭遇了一个 AGI 延迟 (见第9章)。
2.两条指令不能同时访问内存的同一个 DWORD。 下面的例子假定 ESI 可以被 4 整除:
MOV AL, [ESI] / MOV BL, [ESI+1]
两个操作数是在同一个 DWORD 里, 因此它们不能同时执行。 这对指令需要 2 个时钟周期。
MOV AL, [ESI+3] / MOV BL, [ESI+4]
这里两个操作数分别处于两个 DWORD 的边界, 因此它们完美地配对, 只需要消耗 1 个时钟周期。
3. 第 2 条款可以扩展到两个地址的 2-4 位相同的情况 (cache行冲突)。 对于 DWORD 地址, 这意味着两个地址差不能被 32 整除。 例如:
MOV [ESI], EAX / MOV [ESI+32000], EBX ; 有缺陷配对 MOV [ESI], EAX / MOV [ESI+32004], EBX ; 完美配对
不访问内存的配对整型指令可以在一个时钟周期执行完,但是预测失败的跳转例外。 读/写内存的MOV指令,当数据区在cache里并严格对齐的时候也只需要一个时钟周期,即使用了像比例变址寻址这样复杂的寻址模式,也不会有速度上的惩罚。
一组配对整数指令, 如果需要读内存, 做一些计算后把结果保存在寄存器或标记寄存器中时, 需要消耗两个时钟周期。 (读/修改 指令)。
一组配对整数指令, 如果需要读内存, 做一些计算后把结果回写到内存中, 需要消耗3个时钟周期。 (读/修改/写 指令)。
4.如果一条 读/修改/写 指令和一条 读/修改 或 读/修改/写指令 配对, 那么它们就是一个有缺陷配对。
下表展示了各种情况下需要的时钟周期数:
第一条指令 | 第二条指令 | ||
MOV 或者 仅仅是寄存器操作 | 读/修改 | 读/修改/写 | |
MOV 或仅仅是寄存器操作 | 1 | 2 | 3 |
读/修改 | 2 | 2 | 3 |
读/修改/写 | 3 | 4 | 5 |
例如:
ADD [mem1], EAX / ADD EBX, [mem2] ; 4 个时钟周期 ADD EBX, [mem2] / ADD [mem1], EAX ; 3 个时钟周期
5.当两条配对指令都因为cache失效,没有对齐,或跳转预测失败等情况而需要额外时间时, 一对指令消耗的时间将比其中任何一条需要的时间都长, 但是比两条指令需要的时间之和短。
6.在可配对浮点指令之后与其配对的FXCH指令, 当下一条指令不是浮点指令时组成一个缺陷配对。
为了避免有缺陷配对,你必须知道哪条指令进入了 U 管道, 哪条进入了 V 管道。 为此,你可以向前看看你的代码,找到那些不能配对的,或者只能在一条管道中配对, 又或因为上面提及的规则而不能配对的指令,这样就可以清楚地知道后面的指令中哪条指令进入了 U 管道, 哪条进入了 V 管道。
有缺陷配对通常可以通过重组你的指令来避免。 例如:
L1: MOV EAX,[ESI] MOV EBX,[ESI] INC ECX
这里两条 MOV 指令组成了一个有缺陷配对, 因为它们访问了同一内存地址, 所以这组指令需要消耗 3 个时钟周期。 你可以通过重组指令, 把 INC ECX 跟其中一个 MOV 指令配对。
L2: MOV EAX,OFFSET A XOR EBX,EBX INC EBX MOV ECX,[EAX] JMP L1
INC EBX / MOV ECX,[EAX] 这对指令是一个有缺陷配对, 因为后一条指令发生了 AGI 延迟。 这组指令消耗 4 个时钟周期。 如果你插入一条 NOP 或任意别的指令, 使得MOV ECX,[EAX] 跟 JMP L1 配对, 这样这组指令就只需要消耗 3 个时钟周期了。
下一个例子是 16 位模式下的, 假设 SP 可以被 4 整除:
L3: PUSH AX PUSH BX PUSH CX PUSH DX CALL FUNC
这里 PUSH 指令组成了两个有缺陷配对, 因为各对指令中的两个操作数都放入了内存的同一 DWORD 中。 PUSH BX 可能可以和 PUSH CX 完美配对起来 (因为它们访问的是两个不同的 DWORD) 但是并不是这样, 因为它已经和 PUSH AX 配对了。 这组指令消耗了 5 个时钟周期。
如果你插入一个 NOP 或者其它指令, 让 PUSH BX 跟 PUSH CX 配对, 而 PUSH DX 和 CALL FUNC 配对, 这样这组指令就只需要 3 个时钟周期了。 另一个解决方案是,让SP不被 4 整除。 想知道 SP 是否被 4 整除在 16 位模式下是很困难的, 所以避免这个问题的最佳方案是去使用 32 位模式。