4 Pentium 1 and Pentium MMX pipeline
先说说当时 的时代背景,90年代初,由于当时的存储容量昂贵,制作工艺水平低下。所以当时risc的风头很猛,大有一超cisc的意思。当时市场上出现了各种各样的cpu,具体型号类型就不说了。intel那时候的产品线都是x86系列,根本谈不上高性能。只有risc才是高性能的代名词。甚至连intel自己内部也出现了不同分歧,所以才有了IA-64 指令集和Itanium 处理器。可以说当时intel的日子确实不好过,而其它厂家则感觉拿下intel的时日就在眼前了。尽管risc被鼓吹的很高,并不代表risc产品就那么容易做出来。就如同我们现在看别人家的产品,感觉做的一般,自己也能做出来,但是当你实际操作时,就会感觉怎么那么难啊。当然intel再接下来的日子也没有吃干饭。埋头苦干。当中为intel立下头功的是以色列团队。以色列这个国家牛x啊,地小,人少,但是世界各国家的大企业几乎都在这里设有研发机构。这里有着并不比硅谷差的科技氛围。大家可以查查看,现在科技界的大佬们几乎在以色列都有分支机构。intel是最早期就看上这个小国家的企业。intel以色列团队在intel一筹莫展时带来了拨云见日的东东,这就是intel Pentium第一代机器。并且以色列团队也引领了接下来几代intel产品的风骚势头。不过,就当时的情形,即时这款在intel历史上有举足重轻的cpu和当时risc结构的powerpc和mips比,也落后很多,当时超标量,高性能流水线都是risc大的专有代名词。尽管这款cpu的存在历史并不很长,但是这款产品对于intel来说确有着特殊的意义:1 intel x86处理器开始在历史舞台向高性能迈进;
2 intel x86处理器开始正面抗衡risc处理器;
3 intel x86兼容性设计逐渐成为其它厂家的隐患;
4 intel x86处理器笔记本开始普及;
5 intel x86处理器开始向多核,多处理器靠拢;
关于第3条,当然是因为intel的垄断,但是也有别的原因,那就是指令集复杂,没有标准,或者说屎山越来越高,只能往上堆,别人想来试试水,根本就进不去。奇怪就在于此,一家企业标准不统一,竟然也没有其它商家能超越。不过,这也可能是intel的套路。对此有兴趣的同学,可以网络查阅相关信息确定。总之就是,intel自此慢慢强大。尽管当时很多risc产品中都有pipeline设计,但是intel的486是x86系列首款真正实现指令流水线的处理器。而93年的Pentium则是intel第一款超标量处理器,奔腾处理器架构增加了第二条独立的流水线。主流水线工作方式类似于 i486,第二条流水线则并行的运行一些较简单的指令,比如说定点算术。下面回到正文。
P1和PMMX处理器没有乱序功能。 但是它们可以通过指令配对机制同时执行两个连续的指令,具体如下描述。
4.1 整形指令配对
指令配对分为完全配对和不完全配对。完全配对
P1和PMMX具有两个执行指令的管道,分别称为U-pipe和V-pipe。在某些条件下,可以同时执行两个指令,一个在U-pipe中,一个在V-pipe中。这几乎可以使速度提高一倍。当然对同时执行的指令有要求,并不是任意指令都能同时执行。因此,需要对指令重新排序以使其成对执行。我这是根据字面意思翻译的,其实就是2条pipeline,也就是最初级的超标量,并且2条流水线上同时执行的指令有一定的要求,需要完全配对,后面会描述不完全配对的情形。下面的指令在不管在U-pipe还是V-pipe都会配对执行,就是说只要下面描述的指令,任意2条只要是顺序排列,不管指令是在U-pipe还是V-pipe,都可以完全配对,高效执行。
? MOV register, memory, or immediate into register or memory
? PUSH register or immediate, POP register
? LEA, NOP
? INC, DEC, ADD, SUB, CMP, AND, OR, XOR,
? and some forms of TEST (See manual 4: "Instruction tables").
下面的指令要配对执行,必须进入U-pipe:
? ADC, SBB
? SHR, SAR, SHL, SAL with immediate count
? ROR, ROL, RCR, RCL with an immediate count of 1
下面的指令可以在任何pipe执行,但是如果要配对执行,必须进入V-pipe:
? near call
? short and near jump
? short and near conditional jump.
其它所有的整形指令只能进入U-pipe执行,不能配对。
由此可见,U-pipe是一条完整功能的流水线,而V-pipe只是一条辅助流水线,没有U-pipe那么强大。
如何排列指令才能按照上面描述进入U或 V-pipe执行呢?继续往下看。
当满足下面所列条件时,两条前后连续的指令将配对:
1.第一条指令在U管道中,第二条指令在V管道中,才能配对执行。
2.第二条指令不读或写第一条指令要写入的寄存器。
点击(此处)折叠或打开
- ; Example 4.1a. P1/PMMX pairing rules
- mov eax, ebx / mov ecx,eax ; Read after write, do not pair
- mov eax, 1 / mov eax,2 ; write after write,do not pair
- mov ebx, eax / mov eax,2 ; Write after read, pair ok
- mov ebx, eax / mov ecx,eax ; Read after read, pair ok
- mov ebx, eax / inc eax ; Read and write after read, pair ok
3.对于规则2,操作同一个寄存器的不同部分等同于操作整个寄存器,看例子即可明白。
点击(此处)折叠或打开
- ; Example 4.1b. P1/PMMX pairing rules
- mov al, bl / mov ah, 0
4.尽管有规则2和规则3,如果两条指令都写入的是标志寄存器,只要是不同的标志位,则可以配对。
点击(此处)折叠或打开
- ; Example 4.1c. P1/PMMX pairing rules
- shr eax, 4 / inc ebx ; pair OK
5.第一条指令修改标志,第二条是根据这个标志跳转指令,则可以配对,也就是说写后读,规则2无效。
点击(此处)折叠或打开
- ; Example 4.1d. P1/PMMX pairing rules
- cmp eax, 2 / ja LabelBigger ; pair OK
点击(此处)折叠或打开
- ; Example 4.1e. P1/PMMX pairing rules
- push + push
- push + call
- pop + pop
MOVZX,MOVSX,PUSH FS,POP FS,PUSH GS,
POP GS,LFS,LGS,LSS,SETcc,BT,BTC,BTR,BTS,BSF,BSR,SHLD,SHRD和有两个操作数、没有立即数的IMUL指令。
在P1上,除了带有条件near jumps指令外,其它带前缀的指令只能在U-pipe中执行;
在PMMX上,具有操作数大小或地址大小前缀的指令可以在任一pipe中执行,而具有segment,repeat或lock前缀的指令只能在U-pipe中执行。
8.既有偏置数又有立即数的指令在P1上不能配对,在PMMX上配队的话,指令必须进入U-pipe:
点击(此处)折叠或打开
- ; Example 4.1f. P1/PMMX pairing rules
- mov dword [ds:1000], 0 ; Not pairable or only in u-pipe
- cmp byte [ebx+8], 1 ; Not pairable or only in u-pipe
- cmp byte [ebx], 1 ; Pairable
- cmp byte [ebx+8], al ; Pairable
9.所有的两条指令都必须预先加载和解码。 否则这不会在P1上发生,除非第一条指令只有一个字节长度。
10.PMMX上的MMX指令有特殊的配对规则:
?MMX移位,压缩或解压缩指令可以在任一管道中执行,但不能与其他MMX移位,压缩或解压缩指令配对。
?MMX乘法指令可以在任一管道中执行,但不能与其他MMX乘法指令配对。 它们需要3个时钟周期,最后2个时钟周期可以与后续指令重叠,方式与浮点指令相同(请参见第45页)。
?访问内存或整数寄存器的MMX指令只能在U-pipe中执行,并且不能与非MMX指令配对。
不完全配对
在某些情况下,U-pipe和V-pipe中的一对指令中的两条指令将不会同时执行,或者在时间上只会部分重叠。 但是,它们仍应视为一对,因为第一条指令在U-pipe中执行,第二条指令在V-pipe中执行。 后续指令在不完全配对中的两条指令都完成之前不会开始执行。在下列情况下会产生不完全配对:1.如果第二条指令出现AGI停顿(关于AGI看后面)。
2.两条指令不能同时访问同一个DWORD长度的内存,哪怕访问地址不同。
点击(此处)折叠或打开
- ; Example 4.2a. P1/PMMX imperfect pairing
- mov al, [esi] / mov bl, [esi+1]
点击(此处)折叠或打开
- ; Example 4.2b. P1/PMMX perfect pairing
- mov al, [esi+3] / mov bl, [esi+4]
3.上面描述的是缓存命中的情况。如果两个地址不能同时命中的话,如下描述:
第一代Pentium处理器高速缓存行是由2个16字节组成的32字节大小,可以认为是一组2个行,每行16字节。而现在的intel处理器高速缓存一般都是一行64字节,多组级联。所以如果两个地址偏差不是32字节,那就可能会产生缓存冲突。产生冲突的时候时钟情况如下:
3.1.除错误的跳转外,不访问存储器的可配对整数指令需要一个时钟周期执行。 如果数据区域在高速缓存中并正确对齐,则读或写存储器的MOV指令也仅需要一个时钟周期。 即使使用复杂的寻址模式(例如比例索引寄存器寻址)也不会造成速度损失。
3.2.从内存读取,进行一些计算并将结果存储在寄存器或标志寄存器中的可配对整数指令需要2个时钟周期。 (读/修改指令)。
3.3.从内存读取,进行一些计算并将结果写回内存的可配对整数指令需要3个时钟周期。 (读/修改/写指令)。
这一段理解可能有误 ,如果缓存冲突,从二级或内存读取数据,可能不止是这么几个时钟就够的。
4.如果读/修改/写指令与读/修改指令、或读/修改/写指令配对,那么这是不完全配对。时钟周期使用情况如下:
5.当两条已配对的指令由于高速缓存未命中,未对齐或跳转预测错误而都需要花费额外的时间时,则该对指令将花费更多的时间,比执行两者任一指令都要长,但少于两者分别执行所需时间的总和。
点击(此处)折叠或打开
- Examples:
- ; Example 4.3. P1/PMMX pairing complex nstructions
- add [mem1], eax / add ebx, [mem2] ; 4 clock cycles
- add ebx, [mem2] / add [mem1], eax ; 3 clock cycles
为了避免不完全配对,你必须了解哪些指令进入U-pipe,哪些指令进入V-pipe。究竟哪条指令进入哪个pipe,您可以通过以下方式 来确定并找出原因:
6.1 查看当前指令后面的代码并找出不可配对的指令
6.2 只能在其中一个管道中可配对
6.3 由于上述规则之一而无法配对的指令
不完全配对通常可以通过指令重拍序来避免,如:
点击(此处)折叠或打开
- ; Example 4.4. P1/PMMX reorder instructions to improve pairing
- L1: mov eax,[esi]
- mov ebx,[esi]
- inc ecx
点击(此处)折叠或打开
- ; Example 4.5. P1/PMMX reorder instructions to improve pairing
- L2: mov eax,offset a
- xor ebx,ebx
- inc ebx
- mov ecx,[eax]
- jmp L1
下面的例子是16位模式,假设sp是4字节对齐的。
点击(此处)折叠或打开
- ; Example 4.6. P1/PMMX imperfect pairing, 16 bit mode
- L3: push ax
- push bx
- push cx
- push dx
- call Func
4.2 地址生成互锁(AGI)
访问存储器的指令,一般需要一个时钟周期来计算所需的地址。 通常,此计算是在流水线执行前一条指令或指令对时的某一阶段完成的。 但是,如果地址取决于前一个时钟周期中执行的指令的结果,则我们必须等待一个额外的时钟周期。 这称为AGI停顿。
上面例子中的停顿可以通过在两条指令之间添加指令或者改进该段代码去除,下面是改进代码:
如果前一时钟周期修改了ESP,如mov,add或sub指令,接下来的指令隐含的使用SP来寻址,也可能会造成AGI停顿,如PUSH,POP,CALL和RET。P1和PMMX具有特殊的电路,可在堆栈操作后预测ESP的值,这样在使用PUSH,POP或CALL更改ESP后,您不会得到AGI延迟。 仅当ret操作ESP涉及到立即数时,才能在RET之后获得AGI停顿。如:
LEA指令使用了基址或索引寄存器,并且这个寄存器在前面紧接着的指令时钟周期进行了修改,那么LEA指令也会产生一个AGI停顿。如:
PPro,P2和P3对于内存读取和LEA指令,没有AGI停顿,但对内存写入有AGI停顿。 除非后续代码必须等待写入完成,否则这不是很重要。
点击(此处)折叠或打开
- ; Example 4.7a. P1/PMMX AGI
- add ebx,4
- mov eax,[ebx] ; AGI stall
点击(此处)折叠或打开
- ; Example 4.7b. P1/PMMX AGI removed
- mov eax,[ebx+4]
- add ebx,4
点击(此处)折叠或打开
- ; Example 4.8. P1/PMMX AGI
- add esp,4 / pop esi ;AGI stall
- pop eax / pop esi ;no stall, pair
- mov esp,ebp / ret ;AGI stall
- call F1 / F1: mov eax,[esp+8] ;no stall
- ret / pop eax ;no stall
- ret 8 / pop eax ;AGI stall
点击(此处)折叠或打开
- ; Example 4.9. P1/PMMX AGI
- inc esi / lea eax,[ebx+4*esi] ; AGI stall
4.3 复杂指令拆分为简单的多个指令
这么做纯粹是为了完全配对进入2条流水线,进而提高效率。当时还没有考虑到要把指令分解为risc风格的uops,然后高效执行。但是这种分解为risc风格的操作确实在孙子代Pentium上实现了,只是第一代上真没有。你可以对read/modify和read/modify/write指令进行拆分来促进配对机制。如:
点击(此处)折叠或打开
- ; Example 4.10a. P1/PMMX Imperfect pairing
- add [mem1],eax
- add [mem2],ebx
点击(此处)折叠或打开
- ; Example 4.10b. P1/PMMX Imperfect pairing avoided
- mov ecx,[mem1]
- mov edx,[mem2]
- add ecx,eax
- add edx,ebx
- mov [mem1],ecx
- mov [mem2],edx
点击(此处)折叠或打开
- ; Example 4.11a. P1/PMMX Non-pairable instructions
- push [mem1]
- push [mem2] ; Non-pairable
点击(此处)折叠或打开
- ; Example 4.11b. Split nonpairable instructions into pairable ones
- mov eax,[mem1]
- mov ebx,[mem2]
- push eax
- push ebx ; Everything pairs
; Example 4.12. P1/PMMX Split non-pairable instructions
CDQ 修改为: mov edx,eax / sar edx,31
not eax 修改为 xor eax,-1
neg eax修改为 xor eax,-1 / inc eax
movzx eax,byte [mem] 修改为 xor eax,eax / mov al,byte [mem]
jecxz L1 修改为 test ecx,ecx / jz L1
loop L1 修改为 dec ecx, / jnz L1
xlat 修改为 mov al,[ebx+eax]
如果拆分指令不能提高速度,则可以保留复杂或不可配对的指令,以减小代码大小。 以后的处理器不需要拆分指令,除非拆分指令生成的μop较少。
4.4 前缀指令
带有一个或多个前缀的指令可能不会在V-pipe中执行,并且译码所用时钟周期数会多于1个时钟周期。
对于P1,除了带0FH前缀的条件near jump指令,其它每带一个前缀,解码时就产生一个时钟周期的延迟。
PMMX对于0FH前缀没有解码延迟。 segment和repeat前缀需要多花一个时钟来解码。 地址和操作数大小前缀需要多花两个时钟来解码。如果第一条指令具有segment或repeat前缀或没有前缀,而第二条指令没有前缀,则PMMX可以在每个时钟周期解码两条指令。 具有地址或操作数大小前缀的指令只能在PMMX上解码。 带有多个前缀的指令每个前缀要多花一个时钟。
在前缀不可避免的情况下,如果前一条指令执行要花费一个以上的时钟周期,则可以掩盖当前指令的解码延迟。 P1的规则是,任何需要花费N个时钟周期执行(不是解码)的指令都可以“掩盖”下两个(有时是三个)指令或指令对中N-1个前缀的解码延迟。 换句话说,一条指令执行的时候,每多执行一个时钟周期就可多解码后一条指令中的一个前缀。 这种掩盖效果甚至延伸到了分支预测上。 任何需要执行一个以上时钟周期的指令,以及由于AGI停顿,高速缓存未命中,未对齐或任何其他原因而延迟的任何指令都具有这种屏蔽掩盖效果。除解码延迟和分支错误预测以外。
PMMX具有类似的屏蔽掩盖效果,但是机制不同。 PMMX解码后的指令存储在对用户透明的先进先出(FIFO)缓冲区中,该缓冲区最多可容纳四条指令。 只要FIFO缓冲区中有指令,您就不会延迟。当缓冲区为空时,指令将在解码后立即执行。 当指令的解码速度比执行速度快时,如当指令是未配对或多周期指令时,缓冲区将被填充。 当指令的执行速度比解码速度快时,如由于前缀而导致解码延迟时,FIFO缓冲区将被清空。 分支预测错误后,FIFO缓冲区清空。FIFO缓冲区可以在每个时钟周期接收两条指令,但是第二条指令要求没有前缀并且所有指令均不超过7个字节。两个执行pipe(U和V)每个时钟周期可以分别从FIFO缓冲区接收一条指令。
cld指令需要两个时钟周期,因此可能会掩盖rep前缀的解码延迟。 如果将cld指令放置在远离rep movsd的位置,则该代码将多花费一个时钟周期。
cmp指令执行花费两个时钟周期,因为它是读/修改指令。 setnz指令的0FH前缀在cmp指令的第二个时钟周期内解码,因此在P1处理器上,解码延迟被隐藏(PMMX没有0FH的解码延迟)。
对于P1,除了带0FH前缀的条件near jump指令,其它每带一个前缀,解码时就产生一个时钟周期的延迟。
PMMX对于0FH前缀没有解码延迟。 segment和repeat前缀需要多花一个时钟来解码。 地址和操作数大小前缀需要多花两个时钟来解码。如果第一条指令具有segment或repeat前缀或没有前缀,而第二条指令没有前缀,则PMMX可以在每个时钟周期解码两条指令。 具有地址或操作数大小前缀的指令只能在PMMX上解码。 带有多个前缀的指令每个前缀要多花一个时钟。
在前缀不可避免的情况下,如果前一条指令执行要花费一个以上的时钟周期,则可以掩盖当前指令的解码延迟。 P1的规则是,任何需要花费N个时钟周期执行(不是解码)的指令都可以“掩盖”下两个(有时是三个)指令或指令对中N-1个前缀的解码延迟。 换句话说,一条指令执行的时候,每多执行一个时钟周期就可多解码后一条指令中的一个前缀。 这种掩盖效果甚至延伸到了分支预测上。 任何需要执行一个以上时钟周期的指令,以及由于AGI停顿,高速缓存未命中,未对齐或任何其他原因而延迟的任何指令都具有这种屏蔽掩盖效果。除解码延迟和分支错误预测以外。
PMMX具有类似的屏蔽掩盖效果,但是机制不同。 PMMX解码后的指令存储在对用户透明的先进先出(FIFO)缓冲区中,该缓冲区最多可容纳四条指令。 只要FIFO缓冲区中有指令,您就不会延迟。当缓冲区为空时,指令将在解码后立即执行。 当指令的解码速度比执行速度快时,如当指令是未配对或多周期指令时,缓冲区将被填充。 当指令的执行速度比解码速度快时,如由于前缀而导致解码延迟时,FIFO缓冲区将被清空。 分支预测错误后,FIFO缓冲区清空。FIFO缓冲区可以在每个时钟周期接收两条指令,但是第二条指令要求没有前缀并且所有指令均不超过7个字节。两个执行pipe(U和V)每个时钟周期可以分别从FIFO缓冲区接收一条指令。
点击(此处)折叠或打开
- ; Example 4.13. P1/PMMX Overshadow prefix decoding delay
- cld
- rep movsd
点击(此处)折叠或打开
- ; Example 4.14. P1 Overshadow prefix decoding delay
- cmp dword [ebx],0
- mov eax,0
- setnz al
4.5 浮点指令代码调度
浮点指令不能如同整型指令那样进行配对,但是有一个特殊情况,定义如下:
?第一条指令(在U管道中执行)必须为FLD,FADD,FSUB,FMUL,FDIV,FCOM,FCHS或FABS。
?第二条指令(在V管道中)必须为FXCH。
?FXCH后面的指令必须是浮点指令,否则FXCH将不完全配对,并需要一个额外的时钟周期。
这种特殊的配对很重要,稍后说明。
虽然通常情况下浮点指令无法配对,但可以对许多指令进行流水线处理,即一条指令可以在上一条指令完成之前开始。如:
显然,如果第二条指令需要第一条指令的结果,则两条指令不能重叠。由于几乎所有浮点指令都涉及堆栈寄存器顶部的ST0,因此,使指令完全独立于先前指令的结果的可能性似乎很小。解决此问题的方法是寄存器重命名。 FXCH指令实际上并不交换两个寄存器的内容;相反,FXCH指令只交换它们的名字。push或pop寄存器堆栈的指令也可以通过重命名来工作。浮点寄存器重命名已在奔腾系列处理器上进行了高度优化,可以在使用时重命名寄存器。寄存器重命名绝不会导致停顿-甚至可以在同一时钟周期内多次重命名一个寄存器,例如FLD或FCOMPP与FXCH配对时。
正确使用FXCH指令,在浮点代码序列中可能会出现大量合理重叠。 指令FADD,FSUB,
FMUL和FILD的所有版本都需要3个时钟周期,并且可以重叠,因此可以对这些指令进行重排序进而合理调度。 如果内存操作数在1级缓存中并且已正确对齐,则使用内存操作数所花的时间不会比寄存器操作数多。
到现在为止,你必须习惯于任何规则都具有例外的情况,这里指令重叠的规则也不例外:您不能在一个FMUL指令之后的一个时钟周期开始另一个FMUL指令,因为FMUL电路不是完美的流水线。 建议您在两个FMUL之间插入另一条指令。
这里,在fmul [b2]和fmul [c2]前都有一个停顿,因为在他们前面已经开始了一个fmul指令。你可以在两个fmul之间插入一个fld指令来改善代码:
遇到类似的其它场景,你可以添加fadd,fsub或其它在多个fmul之间里避免停顿。
并非所有浮点指令都可以重叠。 与浮点指令后继重叠相比较,某些浮点指令后可以重叠更多的整数指令。 例如,FDIV指令需要39个时钟周期。 除了第一个时钟周期外,所有时钟周期都可以与整数指令重叠,但是只有最后两个时钟周期可以与浮点指令重叠。 手册4:“指令表”中给出了浮点指令的完整列表,以及它们可以配对或重叠的内容。
浮点指令使用存储操作数不会带来任何性能损失,因为在流水线中算术单元比读取单元晚一级。但是当将浮点数值存储到内存时,也就是说写内存时,就要进行权衡了。具有存储操作数的FST或FSTP指令在执行阶段需要两个时钟周期,但是它需要提前一个时钟周期准备好数据,因此,如果前一个时钟周期没有准备好要存储的值,则将导致一个时钟周期的停顿。这类似于AGI档。在许多情况下,如果不将浮点代码调度到四个线程中或在它们之间放入一些整数指令,就无法隐藏这种类型的停顿。把浮点代码调度到四个线程是说把连续浮点指令分到多个cpu核分开执行的意思。FST(P)指令执行阶段的两个时钟周期不能与任何后续指令成对或重叠。
带有整数操作数的指令(例如FIADD,FISUB,FIMUL,FIDIV,FICOM)可以拆分为更简单的操作,以改善重叠,提高性能。如:
点击(此处)折叠或打开可以拆分为:
在此示例中,我们通过重叠两个FILD指令来节省两个时钟。
?第一条指令(在U管道中执行)必须为FLD,FADD,FSUB,FMUL,FDIV,FCOM,FCHS或FABS。
?第二条指令(在V管道中)必须为FXCH。
?FXCH后面的指令必须是浮点指令,否则FXCH将不完全配对,并需要一个额外的时钟周期。
这种特殊的配对很重要,稍后说明。
虽然通常情况下浮点指令无法配对,但可以对许多指令进行流水线处理,即一条指令可以在上一条指令完成之前开始。如:
点击(此处)折叠或打开
- ; Example 4.15. Pipelined floating point instructions
- fadd st1,st0 ; Clock cycle 1-3
- fadd st2,st0 ; Clock cycle 2-4
- fadd st3,st0 ; Clock cycle 3-5
- fadd st4,st0 ; Clock cycle 4-6
正确使用FXCH指令,在浮点代码序列中可能会出现大量合理重叠。 指令FADD,FSUB,
FMUL和FILD的所有版本都需要3个时钟周期,并且可以重叠,因此可以对这些指令进行重排序进而合理调度。 如果内存操作数在1级缓存中并且已正确对齐,则使用内存操作数所花的时间不会比寄存器操作数多。
到现在为止,你必须习惯于任何规则都具有例外的情况,这里指令重叠的规则也不例外:您不能在一个FMUL指令之后的一个时钟周期开始另一个FMUL指令,因为FMUL电路不是完美的流水线。 建议您在两个FMUL之间插入另一条指令。
点击(此处)折叠或打开
- ; Example 4.16a. Floating point code with
- fld qword [a1] ; Clock cycle 1
- fld qword [b1] ; Clock cycle 2
- fld qword [c1] ; Clock cycle 3
- fxch st2 ; Clock cycle 3
- fmul qword [a2] ; Clock cycle 4-6
- fxch st1 ; Clock cycle 4
- fmul qword [b2] ; Clock cycle 5-7 (stall)
- fxch st2 ; Clock cycle 5
- fmul qword [c2] ; Clock cycle 7-9 (stall)
- fxch st1 ; Clock cycle 7
- fstp qword [a3] ; Clock cycle 8-9
- fxch st1 ; Clock cycle 10 (unpaired)
- fstp qword [b3] ; Clock cycle 11-12
- fstp qword [c3] ; Clock cycle 13-14
点击(此处)折叠或打开
- ; Example 4.16b. Floating point stalls filled with other instructions
- fld qword [a1] ; Clock cycle 1
- fmul qword [a2] ; Clock cycle 2-4
- fld qword [b1] ; Clock cycle 3
- fmul qword [b2] ; Clock cycle 4-6
- fld qword [c1] ; Clock cycle 5
- fmul qword [c2] ; Clock cycle 6-8
- fxch st2 ; Clock cycle 6
- fstp qword [a3] ; Clock cycle 7-8
- fstp qword [b3] ; Clock cycle 9-10
- fstp qword [c3] ; Clock cycle 11-12
并非所有浮点指令都可以重叠。 与浮点指令后继重叠相比较,某些浮点指令后可以重叠更多的整数指令。 例如,FDIV指令需要39个时钟周期。 除了第一个时钟周期外,所有时钟周期都可以与整数指令重叠,但是只有最后两个时钟周期可以与浮点指令重叠。 手册4:“指令表”中给出了浮点指令的完整列表,以及它们可以配对或重叠的内容。
浮点指令使用存储操作数不会带来任何性能损失,因为在流水线中算术单元比读取单元晚一级。但是当将浮点数值存储到内存时,也就是说写内存时,就要进行权衡了。具有存储操作数的FST或FSTP指令在执行阶段需要两个时钟周期,但是它需要提前一个时钟周期准备好数据,因此,如果前一个时钟周期没有准备好要存储的值,则将导致一个时钟周期的停顿。这类似于AGI档。在许多情况下,如果不将浮点代码调度到四个线程中或在它们之间放入一些整数指令,就无法隐藏这种类型的停顿。把浮点代码调度到四个线程是说把连续浮点指令分到多个cpu核分开执行的意思。FST(P)指令执行阶段的两个时钟周期不能与任何后续指令成对或重叠。
带有整数操作数的指令(例如FIADD,FISUB,FIMUL,FIDIV,FICOM)可以拆分为更简单的操作,以改善重叠,提高性能。如:
点击(此处)折叠或打开
- ; Example 4.17a. Floating point code with integer operands
- fild dword [a]
- fimul dword [b]
点击(此处)折叠或打开
- ; Example 4.17b. Overlapping integer operations
- fild dword [a]
- fild dword [b]
- fmul