19. 部分延迟(PPro,PII 和 PIII)

优质
小牛编辑
122浏览
2023-12-01

19.1 部分寄存器延迟

当你对一个32位寄存器的部分写,不久后读更大的一部分或整个,部分寄存器延迟将发生。 比如:

MOV AL,BYTE PTR [M8]
MOV EBX,EAX      ; 部分寄存器延迟

延迟是5-6个时钟。

理由是一个临时寄存器已被分配给AL(使它独立于AH),在执行单元把AL的值与EAX其余部分的值组合起来进行读以前,必须等待AL写操作的引退。

通过改变代码避免延迟:

MOVZX EBX, BYTE PTR [MEM8]
AND EAX, 0FFFFFF00h
OR EBX, EAX

当然也可以在寄存器的部分写操作之后插入一些其它指令来避免这种延迟,这样在你读整个寄存器之前可以有充分时间引退。

只要你在代码中混合使用了不同的数据尺寸(8,16和32位),你就要注意到部分延迟:

MOV BH, 0
ADD BX, AX     ; 延迟
INC EBX       ; 延迟

如果是先写了大的部分或整个寄存器,然后再读部分寄存器则不会有延迟:

MOV EAX, [MEM32]
ADD BL, AL     ; 无延迟
ADD BH, AH     ; 无延迟
MOV CX, AX     ; 无延迟
MOV DX, BX     ; 延迟

避免部分寄存器延迟的最简单的方法是一直用整个寄存器——在读小的内存操作数时用MOVZX或MOVSX。

这些指令在PPro,PII和PIII上很快,但在早期的处理器上慢。要在所有处理器上运行快得想个折中的办法。 可以把MOVZX EAX,BYTE PTR [M8]替换成如下指令:

XOR EAX,EAX
MOV AL, BYTE PTR [M8]

要知道,为了避免以后读EAX造成的部分寄存器延迟,PPro,PII和PIII专门为此类指令组合做了一件特殊的事,它采取的技巧就是当一个寄存器与自身异或的时候寄存器直接被记为清零。

处理器记住EAX的高24位是零,因此避免了部分延迟。 这个机制只在如下的组合工作:

XOR EAX, EAX
MOV AL, 3
MOV EBX, EAX ; 无延迟
XOR AH, AH
MOV AL, 3
MOV BX, AX ; 无延迟
XOR EAX, EAX
MOV AH, 3
MOV EBX, EAX ; 延迟
SUB EBX, EBX
MOV BL, DL
MOV ECX, EBX ; 无延迟
MOV EBX, 0
MOV BL, DL
MOV ECX, EBX ; 延迟
MOV BL, DL
XOR EBX, EBX   ; 无延迟

通过一个寄存器与它自身相减清零与XOR的工作方式相同,但用MOV指令清零则无法阻止延迟发生。

你可以在循环外写一个XOR指令:

XOR EAX, EAX

MOV ECX, 100

LL:  MOV AL,
[ESI]

MOV [EDI], EAX  ; 无延迟

INC ESI

ADD EDI, 4

DEC ECX

JNZ LL

只要没有中断,预测失败或其它序列化事件发生,处理器会记住EAX的高24位是0。

在调用一个可能会PUSH整个寄存器的子过程之前,你应该记得“压制”任何前不久用过的部分寄存器:

ADD BL, AL

MOV [MEM8], BL

XOR EBX, EBX ; 压制BL

CALL _HighLevelFunction

大多数高级语言会在一个过程开始的地方 PUSH EBX,如果像上面这类情况你没有压制BL,会产生部分寄存器延迟。

用XOR清零一个寄存器的方法并不能打破它对前面指令的依赖:

DIV EBX

MOV [MEM], EAX

MOV EAX, 0     ; 打破依赖

XOR
EAX, EAX    ; 阻止部分寄存器延迟

MOV AL, CL

ADD EBX, EAX

两次把EAX设为0看上去多余,但要知道如果没有MOV EAX,0,那么后续的指令必须等待除法慢指令结束,没有XOR EAX,EAX则会产生部分寄存器延迟。

FNSTSW AX指令是特殊的:在32位模式下它的行为就和写整个EAX一样。事实上,在32位模式下它做的事情类似于:

AND EAX,0FFFF0000h / FNSTSW TEMP / OR EAX,TEMP 因此32位模式下,你在该指令后面读EAX不会发生部分寄存器延迟:

FNSTSW AX / MOV EBX,EAX   ;
只在16位模式下有延迟

MOV AX,0 / FNSTSW AX    ;
只在32位模式下有延迟

19.2 部分标志延迟

标志寄存器也会引起部分寄存器延迟:

CMP EAX, EBX

INC ECX

JBE XX ; 部分标志延迟

JBE指令既读进位标志(CF)又读零标志(ZF)。

因为INC指令修改ZF,不修改CF,在JBE指令结合CF(CMP修改CF)和ZF(INC修改ZF)之前,它必须等待前两条指令的引退。这种情形与其说是故意进行标志的组合,不如说可能是一个bug。

改正bug的方法是用ADD ECX,1取代INC ECX。 类似引起部分标志延迟bug是SAHF / JL XX。

JL指令测试符号标志(SF)和溢出标志(OF),但SAHF指令不改变溢出标志。 改正bug的方法是用JS XX替换JL XX。

出乎意料的是(与Intel手册上说的相反),当你在一条修改了一些标志的指令之后只读一些没有修改过的标志,也会产生部分标志延迟:

CMP EAX, EBX

INC ECX

JC XX     ;
部分标志延迟

但只读修改过标志则没有延迟:

CMP EAX, EBX

INC ECX

JE XX     ; 无延迟

部分标志延迟可能发生在那些读很多标志位的指令上,也就是LAHF,PUSHF,PUSHFD。
以下指令后面若跟LAHF或PUSHF(D)会有部分标志延迟:INC, DEC, TEST, 位 测试, 位扫描, CLC, STC, CMC, CLD,
STD, CLI, STI, MUL, IMUL和所有移位、循环移位。 以下指令不会引起部分标志延迟:AND, OR, XOR, ADD, ADC, SUB,
SBB, CMP, NEG。 奇怪的是TEST和AND的行为不一样的——尽管根据定义,它们确实对标志寄存器做了同样的事情。
你可以用SETcc指令取代LAHF或PUSHF(D)来储存一个标志,避免延迟。

比如:

INC EAX / PUSHFD ; 延迟

ADD EAX,1 / PUSHFD ; 无延迟

SHR EAX,1 / PUSHFD ; 延迟

SHR EAX,1 / OR EAX,EAX / PUSHFD ; 无延迟

TEST EBX,EBX / LAHF ; 延迟

AND EBX,EBX / LAHF ; 无延迟

TEST EBX,EBX / SETZ AL ; 无延迟

CLC / SETZ AL ; 延迟

CLD / SETZ AL ; 无延迟

部分标志延迟的惩罚大约是4个时钟。

19.3 移位和循环移位后的标志延迟

在移位或循环移位后读任意标志位,会发生类似部分标志延迟的延迟,除了移位和循环移位的位数是1且为简易格式(不用计数器)的情况:

SHR EAX,1 / JZ XX ; 无延迟

SHR EAX,2 / JZ XX         ; 延迟

SHR EAX,2 / OR EAX,EAX / JZ XX ; 无延迟

SHR EAX,5 / JC XX         ;延迟

SHR EAX,4 / SHR EAX,1 / JC XX  ; 无延迟

SHR EAX,CL / JZ XX      ;哪怕CL=1也有延迟

SHRD EAX,EBX,1 / JZ XX    ; 延迟

ROL EBX,8 / JC XX       ; 延迟

这种类型的延迟大约是4个时钟。

19.4 部分内存延迟

部分内存延迟与部分寄存器延迟有些相像。当你对同一个内存地址,混合不同尺寸的数据进行操作时发生:

MOV BYTE PTR [ESI], AL

MOV EBX, DWORD PTR [ESI] ; 部分内存延迟

在此,因为处理器必须把AL写回的1个字节和后面3个原来在内存中的字节组合,以得到需要读进EBX的4个字节,所以会发生延迟。

惩罚大约是7-8个时钟。

和部分寄存器延迟不同的是,当你把一个大尺寸的操作数写入内存,然后读其中的一部分且这部分的起始地址与原来不同时,也会有部分内存延迟:

MOV DWORD PTR [ESI], EAX

MOV BL, BYTE PTR [ESI]  ; 无延迟

MOV BH, BYTE PTR [ESI+1] ; 延迟

你可以通过把最后一行改成MOV BH,AH来避免延迟,但这种解决方法在以下情况是做不到的:

FISTP QWORD PTR [EDI]

MOV EAX, DWORD PTR [EDI]

MOV EDX, DWORD PTR [EDI+4] ; 延迟

有趣的是,在写后如果读一个完全不同的地址,只是碰巧与写的地址在不同的cache行有相同组值时,也会有部分内存延迟:

MOV BYTE PTR [ESI], AL

MOV EBX, DWORD PTR [ESI+4092] ; 无延迟

MOV ECX, DWORD PTR [ESI+4096] ; 延迟