|
まず最初に、残念なお話をしなくてはなりません。
私の予測は大きく誤っていました。
浮動小数点というのは、かくも扱いにくい代物なのです。
私は昨日一日かけて、一昨日作った整数から浮動小数点へ手動的に変換するプログラムをつくりました。
この作業は慎重に、段階的に行いました。まず、単独で変換するルーチンをPentiumのアセンブリ言語を使って組み直しました。
_asm{
xor eax,eax
mov ax,fixed
bsr bx,ax
mov ecx,23
mov edx,ebx
sub ecx,edx
add ebx,07fh
shl eax,cl
shl ebx,23
and eax,7FFFFFh
or ebx,eax
mov data,ebx
}
|
このルーチン自体はペアリングが効くので21クロック程度で変換が完了します。
私が試したところこの手順をこれ以上省くのは非常に難しい。中でも実行時間がかかるのはbsr命令です。これはこの短いコードの中で実に10クロックという時間を消費しているのです。また、非ペアリング命令であるため、その直後のmov ecx,23はストールしてしまうのも問題です。
21クロックというと十分な速度のようですが、ひとつの変換につき21クロックも掛かってしまうのはあまり嬉しい状態とは言えません。
なぜなら、浮動小数点命令を使用する際のボトルネックとなるemms命令を実行することによるタスクスイッチ時間は80クロックだからです。
Direct3Dを使おうとするなら、この処理は4つぶん必要になります。ということは、単純計算して21*4=84回です。これではemms命令と浮動小数点命令を使ったところで殆どかわりません。ちなみに浮動小数点命令を使った場合の変換はペアリングが全く使えないのでひとつにつき10クロックとなります。emmsタスクスイッチと合計すると80+10*4=120クロックであり、ノーマルのMMX-Pentiumでは右のルーチンを四回つかったほうがまだわずかに高速といえます。
_asm{ // MMX版
mov ax,fixed // ひとつめのワード数をいれる
mov ecx,23
bsr dx,ax
sub ecx,edx
add edx,07fh
shl eax,cl
movd mm1,edx
and eax,007FFFFFh
psllq mm1,32
movd mm0,eax
mov ax,fixed2 // ふたつめのワード数をいれる
mov ecx,23
psllq mm0,32
bsr dx,ax
sub ecx,edx
add edx,07fh // 二つ目の指数を計算
shl eax,cl
movd mm3,edx
and eax,007FFFFFh
por mm1,mm3
movd mm2,eax
// mm0 ... もととなるワード数がふたつぶん入ったレジスタ
por mm0,mm2
pslld mm1,23
por mm0,mm1
movq buf,mm0
}
|
しかし恐ろしいのはPentiumIIの場合です。この高度に工夫されたプロセッサではなんとemms命令のタスクスイッチは10クロックで終了します。こないだは20クロックと書いたのですが、それは読み間違いで本当は10クロックでした。わずか10クロックです。ということは絶対必要なbsr命令の実行にかかるのと同じクロック数でMMXモードと浮動小数点モードがスイッチできるというわけです。無論、この場合は浮動小数点命令を使ったほうが圧倒的に速く、計算すると10+10*4=50クロックで済むことになります。
ちなみにMMX命令を使って二つのワード整数を同時に変換する場合は考えうる限りの配慮をして41クロックでした(右のリスト)。
これを二つ使う場合は全体で82クロックとなり、そのまんまのバージョンよりは2クロック高速となります。ループコアなのでたとえ2クロックであっても嬉しいけれども、PentiumIIの50クロックには適いません。
仕方がないのでCPUがPentiumIIの場合はルーチンを差し替えないと本当のパフォーマンスはでないことになってしまいます。
困った事態です。
これではMMX命令を使った方がよいのか、それとも使わない方がよいのかがわからなくなってきてしまいます。
それにしても書き込みに関するコストだけで80クロックというのは少々行き過ぎではないでしょうか。
無論対策はないわけでもありません。ひとつは諦めること、もうひとつは以前説明したように全ての頂点を一度ワードで作っておいてから、後でemms命令を発行したあと、また全部書き直すことです。
キャッシュの効く範囲であれば、後者の方が有効と思われます。効かない範囲であれば、メモリの読み書きが倍増してしまう事態は避けられません。
というわけで、この結果は大変ガックシなものになってしまいました。
|
|
|
ところで先日MMXの話題を出したところ、Mitamexさんからメールをいただきました。
みためっくすです。
ところで最近MMXやってるの? 僕もちょっと(1日ほど)はまったけど、
あんまり速くなったという印象を受けなかった。半透明をやってみたんだけど、
半透明の計算よりもメモリアクセスの方が遥かに時間がかかって、普通に計算す
るのとMMXを使うのと、たいして速度に違いが出ないのではないか、という印
象を受けた。
そのプログラムを作った環境はPentium2−233とEDORAMだっ
たので、メモリの速度が圧倒的に遅かった。そのためかもしれない。
ともかく、今の状態では、CPUと周辺のバランスが悪すぎる。Pentiu
m2は馬鹿みたいに高速なのに、メモリがあまりにも遅い。グラフィックデータ
などは1次キャッシュを軽く越えるので、メモリアクセスに時間を取られてしま
うのだと思う。
僕が思うにMMXは失敗だと思う。3D描画は3Dアクセラレーターを使った
ほうが圧倒的に速いし、CPUにすべての処理を任せる、という思想そのものが
間違っていると思う。専門の処理を分散して行うのが、最も効率がよいと思う。
ちなみに、私が1フレームに数十万頂点を表示しようとしていた時は、ほとん
ど加算だけで座標変換をしていました。
Mitamex
|
この手紙が、結果的に福音となりました。
なぜか?そう、いまのアーキテクチャはメモリ・アクセスが異様に遅いのです。
Pentiumシリーズは80x86シリーズの後継なので、そのレジスタセットをずっと引きずってきました。ところが世はRISCブームでなんでもかんでもCPUの汎用レジスタが32本だの128本だのある時代。86シリーズの持つ汎用レジスタはEAX,EBX,ECX,EDX,EDI,ESI・・EBPを含めてもたったの7本という化石的な仕様となっています。
さらに恐るべきは浮動小数点命令。浮動小数点レジスタは8本ありますがなんとスタック構造になっていて、複雑な計算となるとメモリをバシバシアクセスしてしまいます。
たとえば先日公開した「おそらく」Direct3DRMの内部関数相当と思われるルーチンをVisualC++5.0の速度優先モードでコンパイルしたところ、以下のような具合になってしまいます。
?transVertex@@YAHPAU_D3DTLVERTEX@@MMM@Z PROC NEAR ; transVertex, COMDAT
; 782 : tlv->sx = trans_matrix._11 * tx +
; 783 : trans_matrix._21 * ty +
; 784 : trans_matrix._31 * tz +
; 785 : trans_matrix._41 ; // w は必ず1なので乗算は省略できる
fld DWORD PTR _tz$[esp-4]
fld DWORD PTR _tx$[esp-4]
fmul DWORD PTR ?trans_matrix@@3U_D3DMATRIX@@A
fld DWORD PTR _ty$[esp-4]
fxch ST(2)
fmul DWORD PTR ?trans_matrix@@3U_D3DMATRIX@@A+32
; 786 : tlv->sy = trans_matrix._12 * tx +
; 787 : trans_matrix._22 * ty +
; 788 : trans_matrix._32 * tz +
; 789 : trans_matrix._42 ;
fld DWORD PTR _tx$[esp-4]
fxch ST(3)
fmul DWORD PTR ?trans_matrix@@3U_D3DMATRIX@@A+16
fxch ST(1)
faddp ST(2), ST(0)
fld DWORD PTR _tz$[esp-4]
fld DWORD PTR _ty$[esp-4]
fxch ST(2)
faddp ST(3), ST(0)
; 790 : tlv->sz = trans_matrix._13 * tx +
; 791 : trans_matrix._23 * ty +
; 792 : trans_matrix._33 * tz +
; 793 : trans_matrix._43 ;
fld DWORD PTR _tx$[esp-4]
fld DWORD PTR _tz$[esp-4]
fxch ST(4)
fadd DWORD PTR ?trans_matrix@@3U_D3DMATRIX@@A+48
mov ecx, DWORD PTR _tlv$[esp-4]
fstp DWORD PTR [ecx]
fxch ST(4)
fmul DWORD PTR ?trans_matrix@@3U_D3DMATRIX@@A+4
fxch ST(1)
fmul DWORD PTR ?trans_matrix@@3U_D3DMATRIX@@A+36
fld DWORD PTR _ty$[esp-4]
fxch ST(1)
faddp ST(2), ST(0)
fxch ST(2)
fmul DWORD PTR ?trans_matrix@@3U_D3DMATRIX@@A+20
<以下略>
|
ごらんの通り殆どの計算がメモリ参照を伴っています。
しかも、ペアリング可能な命令はfxchというレジスタ入れ替え命令のみで、このルーチン全体でのペアリング率は僅か5パーセントいうことになっています。そして一つの頂点についての総実行クロック数は少なく見積もっても166クロック。他にメモリのペナルティが考えられます。
これが、MMX命令ならばメモリへのアクセスは殆どなくせます。
まず、行列データはMMXレジスタが三本あれば保存し、使いまわすことができます。つまりこのコード中で
fmul DWORD PTR ?trans_matrix@@3U_D3DMATRIX@@A
となっているところなどは全てレジスタアクセスになります。
さらに、当然ですが16ビット乗算ならひとつにつき0.25クロックでできます。・・・とこれは大袈裟ですが、少なくともひとつの座標をもとめるのに乗算命令は一回で済ますことができます。
また、整数演算を正規化する場合のシフト演算も3ついっぺんにできますから経済的です。
そのうえ、私が進めているベクトルの再利用を考慮したデータ形式であれば、頂点データの読み込みすら不要になる場合が増えることになり、メモリアクセスはさらに減らすことができます。
また、固定小数点方式の持つ唯一のオーバーヘッドである正規化のための除算ですが、これは浮動小数点の性質を利用して論理積、シフト、加算、シフト、論理和の5クロックでできることになりますからわざわざ遅い浮動小数点除算を行う必要はありません。しかもこの演算は単純なので64ビット幅で二つの浮動小数点数を一気に処理できます。つまりひとつにつき3クロック程度で可能です。この処理は、浮動小数点に変換する際に計算するのが最良でしょう。
というわけで、MMX命令もそれほど捨てたもんじゃない可能性が出てきました。
メモリアクセスのペナルティが厳しい現代においては、SIMD演算よりもレジスタが多いことの方が重要です。さらに、SIMDならばひとつのレジスタに一本のベクトルをいれることができ、大変経済的ですからこれを利用しない手はありません。
次回は、とりあえず一次変換部分をMMX命令に交換し、どの程度の高速化が可能かについて検討してみたいと思います。
|
|
|
|