24. 规划浮点代码(PPlain和PMMX)
浮点指令无法按整型指令的方法去配对,除了下述规则定义的特殊情况:
- 第一条指令(在U管道中执行)必须是FLD, FADD, FSUB, FMUL, FDIV, FCOM, FCHS, 或 FABS。
- 第二条指令(在V管道中执行)必须是FXCH。
- 跟在FXCH后面的那条指令必须是一条浮点指令,否则FXCH的配对是不完美的,会额外地花去一个时钟。
这种特殊的配对很重要,下面作个简单的解释。
大多数情况下浮点指令无法配对,但很多指令是流水化的,也就是说,一条指令可以在前一条指令尚未完成时就开始。比如:
FADD ST(1),ST(0) ; 时钟周期1-3
FADD ST(2),ST(0) ; 时钟周期2-4
FADD ST(3),ST(0) ; 时钟周期3-5
FADD ST(4),ST(0) ; 时钟周期4-6
显然,如果后一条指令需要用到前一条指令的结果的话,那么它们在时间上无法重叠。
因为几乎所有的浮点指令都与寄存器堆栈的栈顶ST(0)有关,所以看来要经常做到后一条指令独立于前一条指令的结果很难。 解决这个难题的方法就是寄存器重命名。
像FXCH指令其实并没有真正交换两个寄存器的值,它仅仅交换了它们的名字。 对浮点堆栈进行压栈或出栈操作的指令也是通过寄存器重命名工作的。
浮点寄存器重命名在Pentium上被高度优化过,因此寄存器在使用的时候可能被重命名。
寄存器重命名从来不会引起延迟——甚至可能在1个时钟周期中对寄存器重命名多次,就像示例中让FLD或FCOMPP与FXCH配对那样。
通过使用FXCH指令,你就可以获得很多浮点代码的重叠执行。比如:
FLD [a1] ; 时钟周期1
FADD [a2] ; 时钟周期2-4
FLD
[b1] ; 时钟周期3
FADD [b2] ; 时钟周期4-6
FLD [c1] ; 时钟周期5
FADD [c2]
; 时钟周期6-8
FXCH ST(2) ; 时钟周期6
FADD [a3] ; 时钟周期7-9
FXCH
ST(1) ; 时钟周期7
FADD [b3] ; 时钟周期8-10
FXCH ST(2) ; 时钟周期8
FADD
[c3] ; 时钟周期9-11
FXCH ST(1) ; 时钟周期9
FADD [a4] ; 时钟周期10-12
FXCH
ST(2) ; 时钟周期10
FADD [b4] ; 时钟周期11-13
FXCH ST(1) ; 时钟周期11
FADD
[c4] ; 时钟周期12-14
FXCH ST(2) ; 时钟周期12
上面的例子中,我们把3个独立的线程交错。
每个FADD需要3个时钟周期,我们可以在每个时钟周期开始一个新的FADD。
当我们在“a线程”开始一个FADD指令,在回到“a线程”以前,我们还有时间在“b线程”和“c线程”中开始两个新的FADD指令,因此每隔两个的FADD指令属于同一个线程。
就像上面的代码,我们每次用FXCH指令把想操作的线程的寄存器换入ST(0),这产生了一个有规律的模式。
值得注意是FXCH指令的重复周期是2,而线程的重复周期是3。 这很容易搞错,因此要对机器熟悉,知道各个浮点寄存器当前在什么位置。
所有版本的FADD,FSUB,FMUL和FILD指令都花3个周期且可以重叠执行,因此这些指令可以用上面的办法来调度。
如果内存操作数在1级cache中并且完全对齐的话,那么使用内存操作数花的时间不比寄存器操作数多。
你现在一定习惯于带有例外的规则了,上述的重叠规则也有个例外:在一个FMUL指令之后的第一个时钟周期内你不能开始一个新的FMUL,因为FMUL的电路并不是完全流水化的。
推荐你在两条FMUL指令之间插入其它的指令,比如:
FLD [a1] ; 时钟周期1
FLD [b1] ; 时钟周期2
FLD [c1] ;
时钟周期3
FXCH ST(2) ; 时钟周期3
FMUL [a2] ; 时钟周期4-6
FXCH ;
时钟周期4
FMUL [b2] ; 时钟周期5-7 (延迟)
FXCH ST(2) ; 时钟周期5
FMUL
[c2] ; 时钟周期7-9 (延迟)
FXCH ; 时钟周期7
FSTP [a3] ; 时钟周期8-9
FXCH ;
时钟周期10 (未配对)
FSTP [b3] ; 时钟周期11-12
FSTP [c3] ; 时钟周期13-14
这里,你在FMUL
[b2]和FMUL [c2]之前有延迟,因为它们是在前一个FMUL后的第一个时钟周期开始的。你可以在各个FMUL之间插入FLD指令来改进代码:
FLD [a1] ; 时钟周期1
FMUL [a2] ; 时钟周期2-4
FLD
[b1] ; 时钟周期3
FMUL [b2] ; 时钟周期4-6
FLD [c1] ; 时钟周期5
FMUL [c2]
; 时钟周期6-8
FXCH ST(2) ; 时钟周期6
FSTP [a3] ; 时钟周期7-8
FSTP
[b3] ; 时钟周期9-10
FSTP [c3] ; 时钟周期11-12
你也可以在FMUL之间插入FADD,FSUB或其它指令来避免延迟。
当然,重叠浮点指令的前提是你有一些彼此独立的线程能够交错。如果你只有一个很大的公式要执行,那么你可以并行计算公式的每个部分,达到重叠的目的。比如要把6个数相加,你可以分2个线程,每个线程有3个数,最后把两个线程的结果相加:
FLD [a] ; 时钟周期1
FADD [b] ; 时钟周期2-4
FLD [c]
; 时钟周期3
FADD [d] ; 时钟周期4-6
FXCH ; 时钟周期4
FADD [e] ;
时钟周期5-7
FXCH ; 时钟周期5
FADD [f] ; 时钟周期7-9 (延迟)
FADD ; 时钟周期10-12 (延迟)
因为要等FADD [d]的结果,我们在FADD [f]之前有1个时钟延迟;又因为等FADD
[f]的结果,在最后一个FADD之前有2个时钟延迟。
通过在最后一个FADD之前插入一些整型指令可以掩盖第二个延迟,但对于第一个延迟这么做没用,因为整型指令会使FXCH的配对不完美。
开3个线程而不是2个,可以避免第一个延迟。
但这将多出一个FLD指令,因此并没节约,除非相加的数在大于等于8个。
不是所有的浮点指令都能重叠执行的。 有些浮点指令能够覆盖的整型指令比浮点指令多。
除法FDIV就是一个例子,它花39个周期。 在它之后,除第一个周期外的其它周期都能重叠整型指令,但只有最后两个时钟能够重叠浮点指令。比如:
FDIV ; 时钟周期1-39 (U流水线)
FXCH ; 时钟周期1-2 (V流水线,不完美的配对)
SHR EAX,1 ; 时钟周期3 (U流水线)
INC EBX ; 时钟周期3 (V流水线)
CMC
; 时钟周期4-5 (不能配对)
FADD [x] ; 时钟周期38-40 (U流水线,
当FPU忙时只能等)
FXCH ; 时钟周期38 (V流水线)
FMUL [y] ; 时钟周期40-42 (U流水线, 等FDIV的结果)
第一个FXCH与FDIV配对,但要额外花去1个时钟因为后面跟的不是浮点指令。
SHR/INC指令对在FDIV完成前就能开始,但必须等FXCH结束。
FADD指令必须等到第38个时钟才开始,因为新的浮点指令只能在FDIV的最后两个时钟开始执行。
第二个FXCH与FADD是配对的。 FMUL指令必须等FDIV结束,因为它要用到除法的结果。
如果在那种能重叠很多整型指令的浮点指令(比如FDIV或FSQRT)后面你没有什么事可做,那么你可以对后面的程序可能用到的内存地址进行“哑读”,以保证它在1级cache中。比如:
FDIV QWORD PTR [EBX]
CMP [ESI],ESI
FMUL QWORD
PTR [ESI]
在此,当计算除法的时候,我们用整型指令与之重叠,把在[ESI]地址的值预取入cache(我们不关心CMP指令的结果是什么)。
第28章给了一个完整的浮点指令列表,以及它们能与那些指令配对,与那些指令重叠。
在浮点指令中用内存操作数并不花代价,因为在流水线中,运算单元比读取单元慢一步。
但当你把浮点数据存入内存的时候就“不公平”了: 带内存操作数的FST或FSTP指令在执行阶段花两个周期,但要提早1个周期把数据准备好。
如果要存的数据没有提前准备好的话,就会有一个周期的延迟。这与AGI延迟有点像。 比如:
FLD [a1] ; 时钟周期1
FADD [a2]
; 时钟周期2-4
FLD [b1] ; 时钟周期3
FADD [b2] ; 时钟周期4-6
FXCH ;
时钟周期4
FSTP [a3] ; 时钟周期6-7
FSTP [b3] ; 时钟周期8-9
FSTP [a3]将延迟1个时钟,因为FADD [a2]的结果没有提前一个时钟准备好。
多数情况下,要不是通过把浮点代码规划成4个线程或在其中插入一些整型指令的话,这种延迟是无法掩盖的。
此外,FST(P)执行阶段的2个时钟是无法与后续指令配对或重叠的。
带有整型操作数的指令诸如:FIADD, FISUB, FIMUL, FIDIV,
FICOM可以切成简单的操作,以改善重叠。比如:
FILD [a] ; 时钟周期1-3
FIMUL
[b] ; 时钟周期4-9
切成:
FILD [a] ; 时钟周期1-3
FILD [b] ; 时钟周期2-4
FMUL ;
时钟周期5-7
在示例中,通过重叠两条FILD指令你节省了2个时钟。