FPU命令の実行時間

ここではP5やK6のFPU命令の処理時間について大まかに説明します. 最適化手法の違いについても若干述べます.
1. スループット(throughput)とレイテンシ(latency)
スループットとは命令が実行されてから次の命令が実行出来るまでのclk数, レイテンシとは命令が実行されてからその結果を利用できるようになるまでのclk数のことです.
InteL系ではfld/fstは1clk, fadd/fsub, fchs/fabsなどの基本命令は3clkレイテンシ, 1clkスループットです. しかもこれらの命令とfxchとはペアリング実行可能です.
それに対しK6シリーズは殆どの命令が2clkレイテンシ, 2clkスループットです. FPU命令同士はペアリング出来ません. fldが1clkなのか私はよく分かっていません.
どのCPUでも割り算や超越関数の演算等複雑な計算はもっと時間がかかります. またP5では整数演算命令とのペアリングは不可, Ppro, P2, K6では可能です.
2. コーディング例
P5とK6のスループットとレイテンシの違いは最適化にどのような違いが現れるのでしょうか? 簡単な例で考えてみます.
#私はK6-2しか持っていないためP5でのclkは資料または予想値です.
static double a,b,c,d,e,f,g,h;
に対し
a=b+c+d;
e=f+g+h;
を計算する.
case 1. (一番素朴なバージョン)

fld  b      ;1
fadd c      ;2-4
fadd d      ;5-7
fstp a      ;8-10
fld  f      ;11
fadd g      ;12-14
fadd h      ;15-17
fstp e      ;18-20 (P5のclkタイミング)
この場合P5では20clk, K6の場合は2*8=16clkでしょう. (ただK6の実測値は27clkでした. 何故遅いのかは後述)
しかし, aとeの計算は明らかに独立ですからP5ではそれぞれを混ぜオーバーラップさせることで高速化が出来ます. P5のfxchは計算結果が出ていなくてもレジスタリネーミング機構によりペアリング実行可能です.
case 2. (P5用最適化バージョン)

fld b       ;1
fadd c      ;2-4
fld f       ;3
fadd g      ;4-6
fxch st(1)  ;-
fadd d      ;5-7
fxch st(1)  ;-
fadd h      ;6-8
fxch st(1)  ;-
fstp a      ;8-9
fstp e      ;10-11
FPUの2つのユニットを同時に利用するようにするとに11clkで計算できることになります. ただ命令数が増えるためK6では逆に遅くなります. 11*2=22clkはかかるでしょう [実測値は32clk].
P5用の最適化コードはK6にかなり不利なことが分かります. また, この最適化は中途半端に人間がするぐらいならコンパイラに任せたほうがいい結果が出そうな気がします. 少なくとも私はやりたくない(^^;
3. K6向け最適化

ところで何故case 1のK6の実測値は遅かったのでしょうか?
たんに私のマシンのメモリ性能が悪いだけ(:-P)ということは置いておいて, 実は2clkで実行出来るのは命令byte数が短いものだけのようです. これは3D Now!命令でも同様であり, 32bitディスプレースメントを含んだりするとshort decodeされません.
従ってa, b,...が連続してメモリ上にあることを仮定して次の様にする必要があります.
case 1'. (命令長を短くしたバージョン)

mov     edx,offset a   ;lea edx,[a]よりは1byte短いです(^^;
fld      [edx+_b]
fadd     [edx+_c]
fadd     [edx+_d]
fstp     [edx+_a]
fld      [edx+_f]
fadd     [edx+_g]
fadd     [edx+_h]
fstp     [edx+_e]

#_a,_b,...はoffset aからの相対値
case 1.のfadd a等の命令長は6byteなのに対し, 8bidディスプレースメントを使ったcase 1'の場合は3byteですみます. そのため約15clkと理論値通りの値を出しています.
こんな単純な足し算の場合すらP5とK6では全然最適化方法が違うことが分かると思います.
更に, 細かい話になりますがK6の場合, FPU命令が存在しているアドレスがmodulo 32で30, 31byteから始まっている場合にもshort decodeされません. これは命令のprefetchが32byteラインごとに行われるため, その境界を跨ぐとうまくいかないからのようです.
これを防ぐには命令byte数を数えながら, 32byte境界を跨ぎそうになったら命令の順序を入れ換えるかnop命令を挟むことで回避します. ループさせる場合に数clk/1loopの効果が出る場合があります.
4. その他の命令
fiadd等の整数との処理を行う命令はP5ではfild+faddに分解した方が速くなりますが, K6ではそのままのほうが速いです.
(例)
fild  a
fimul b
はP5なら9clk, K6なら4clk
同等の処理として
fild  a
fild  b
fmul
ならP5では7clk, K6なら6clkです.
割り算命令はP5, K6共に約40clk, fsqrtは60〜70clk, 超越関数は20〜100clk程度です. これらの命令はなるべく使わないようにするか, 必要な場合でも出来るだけ計算回数を減らす工夫をアセンブリ言語に落とす前のレベルで行うべきです.
fsqrtが意外に速い, というよりむしろ割算が遅いのに注意します. よく, 平方根を高速に求める方法としてNewton法が紹介されます. しかし, 欲しい精度に到達するまでに普通は3, 4回は割り算を必要とするため素直にfsqrtを使った方が速いことの方が多いです.
もう一つ, 精度が問題ないならdoubleでなくfloatを使うべきです.
C言語のレベルでdoubleでなくfloatを使うと型変換のために遅くなると聞く事がありますが, 少なくともアセンブリ言語レベルでは全く関係有りません. むしろfloatの方が必要メモリが少なくなるためキャッシュヒットしやすくなり速くなります.

御意見は光成滋生<herumi@nifty.com>までお願いします