- Advanced Optimization -

概要

最近のCPUにはFPUは当然としてMMXや3D Now!など面白い機能があります. それなのにDirectXやライブラリを通してしか使わないのは勿体ないし, まどろっこしく感じます. ここでは各命令の説明を交えながら高速化のいろいろな具体例を挙げていく予定です.

私の知識では誤りもあるかと思います. その時は御指摘して下さるとありがたいです.

目次
1. FPU
FPUレジスタは通常の汎用レジスタと違って8個まで値を格納できるスタック形式です. スタックトップをstと書き, 以下st(1), st(2), ... st(7)と表記します. スタック形式なので, 通常はメモリから直接 st(i) (i>0) に読み込むことは出来ません. メモリの値を一端stにpush (読み込み) してからst(i)に移します. 9個以上pushすると例外が発生するので注意します. 不要になった値はstから順にpopして消去します. レジスタの値をメモリに書き込む時も同様で, 通常はstの値だけしか書き込めません. 尚, 汎用レジスタと違ってFPU命令には即値モードはありません. 全てアドレスを指定して値のやり取りをします.
1.1 読み込み / 書き込み
浮動小数を読み書きしたい場合は次の命令を使います.
fld mReal / st(i)
メモリに格納された浮動小数形式で格納された値 (mReal) またはst(i)の値をstにpushします. 命令を実行する前のst(i)は実行後st(i+1)に移ります. mRealには32/64/80bit小数を格納しておきます.
fst mReal / st(i)
stの値をmRealまたはst(i)に複写します.
fstp mReal / st(i)
stの値をmRealまたはst(i)に複写したあとstをpopします. 命令の最後に`p'をつけると命令を実行後stをpopするという意味になります. 以後特に説明しません.
整数を読み書きしたい場合は次の命令を使います.
fild mInt
メモリに格納された符号付き整数の値をstにpushします (格納時に浮動小数形式に変換されます). mIntには16/32/64bit整数を格納しておきます.
fist / fistp mInt
stの値を整数に変換したあとmIntに書き込みます.
その他特殊な値をstにpushする命令があります.
命令pushする値
fld1 1
fldl2e log 2 e
fldl2t log 2 10
fldlg2 log 10 2
fldln2 log 2 (自然対数)
fldpi π
fldz +0
(例)
レジスタが全て空 (free) の状態で
memB dd 100    ;整数
memC dq -0.3   ;倍精度(double)
memD dd 0
                       ;st    ,st(1)  ,st(2)  ,st(3)
fldpi                  ;3.14
fild dword ptr [memB]  ;100.0 ,3.14
fld st(1)              ;3.14  ,100.0  ,3.14
fld qword ptr [memC]   ;-0.3  ,3.14   ,100.0  ,3.14
fstp st(1)             ;-0.3  ,100.0  ,3.14
fstp st                ;100.0 ,3.14
fistp dword ptr [memD] ;3.14   [memD]には整数の100が入る
1.2 四則演算
fadd mReal
stにmRealの値を足します.
fadd st,st(i)
stにst(i)を足します.
fadd / faddp st(i),st
st(i)にstを足します.
fsub, fmul, fdiv
引き算, 掛け算, 割り算に関して同様のことをします.
fsubr mReal
mRealの値からstを引いてstに格納します. 引き算ではA - BとB - Aとは違うので逆向きの命令が用意されています.
fsubr st,st(i)
st(i)からstを引いてstに格納します.
fsubr / fsubrp st(i),st
stからst(i)を引いてst(i)に格納します.
fdivrは割り算に関して同様のことをします.
fiadd, fisub/fisubr, fimul, fidiv/fidivr
stと整数とのやり取りとして同様にこれらの命令があります.
1.3 比較
汎用レジスタのように直接フラグを変更出来ないのでクロックは掛かるし複雑です. 一般的には以下の方法を使います. 特殊な方法はまた後で書きます (最近のコンパイラに勝つにはここで勝負?)
stと0と比較
ftst
fnstsw    ax
and       ah, 40h       ;ahの第7bitにZFが入ってる
jne       st_is_zero    ;0なら分岐
stとst(i)との比較
fcom      st(i)
fnstsw    ax
shr       ah, 1        ;ahの最下位bitにCFが入ってる
jnc       st_lg_sti    ;st>=st(i)なら分岐
整数との比較のficomや命令実行後popするfcomp / fcompp / ficompもあります.
tasm Ver. 3.0でfnstsw axを使うとバグります. 結構はまるので気をつけましょう (経験者は語る). fnstsw axのバイナリコードは0DFh,0E0hです.
1.4 符号処理
fabs
stにstの絶対値を格納します.
fchs
stの符号を反転します.
1.5 スタック処理
fxch st(i)
stとst(i)を交換します.
fstp st
stをpopします.
fcompp
stとst(1)をまとめてpopします.
1.6 平方根や超越関数
fsin, fcos, fsqrtはそれぞれstのsin, cos, √を計算してstに格納します.
fptan
まずtan(st)をpushしてから1.0をpushします.
fpatan
Tan-1(st(1) / st)をst(1)に格納したあとstをpopします.
fyl2x
st(1)*log2(st)をst(1)に格納したあとstをpopします.
(例)
x = stの自然対数(底がe)を求める.

              ;st                     ,st(1)
fldln2        ;log(2)                 ,x
fxch st(1)    ;x                      ,log(2)
fyl2x         ;log(2)*log_2(x)=log(x)
2^xを求める (最適解ではないかもしれません).
F_SIZE equ 8
F_PTR  equ qword ptr

_05    dd    0.5
_1     dd    1.0

in : st0=x
out: st0=2^st0
                          ;st    ;st(1)
 1 sub     esp, F_SIZE    ;local var確保
 2 fld     st             ;x        ;x
 3 fsub    _05            ;x-0.5    ;x
 4 fistp   F_PTR [esp]    ;x
 5 fild    F_PTR [esp]    ;y=[x]    ;x    []はガウス記号
 6 fxch                   ;x        ;y
 7 fsub    st, st(1)      ;r:=x-y   ;y
 8 f2xm1                  ;2^r-1    ;y
 9 fadd    _1             ;2^r      ;y
10 fscale                 ;2^r*2^y  ;y
11 fstp    st(1)          ;2^x
12 add     esp, F_SIZE    ;local var開放
y=2x-1を求める命令はあるのですが, |x|<=1 でなければなりません. そのため x=[x]+r, 0<=r<1 と分割してから 2x=2[x]+r=2[x]・2rと変形して計算します.
3〜5行でxを越えない最大の整数を求めています. 0.5を引いているのはfist命令は通常整数を四捨五入する様に設定されているからです. 整数に丸める命令frndintを使うことも出来ますが, 多分こっちの方が速いです.
8〜9行目で2rを求めています. f2xm1は2x-1を求める命令です. 『何故わざわざ1を引いているんだ?』と思われる方がいらっしゃるかもしれません. これはy=2x-1だとx≒0の所でy≒0になるので精度を保つことが出来るからです.
10行目のfscaleはstにst*2[st(1)]を格納します. 単にstの指数部にst(1)を足すだけです. この命令も実は別の命令に分割して計算した方が速いのですが, ややこしいので省略します.
1.7 その他の命令
コントロールレジスタやステータスレジスタの値を設定する命令がいくつかあります. 普通の計算では使わないのでここでは省略します.
1.8 浮動小数の格納形式
3D Now!を使う時はfloatやdoubleがメモリ上でどういう形で格納されているのかを知っている必要があります.
符合bit [s] 指数bit [e] 仮数bit [f]
float 31 30〜23 22〜0 (-1)s・2e-127・1.f
double 63 62〜52 51〜0 (-1)s・2e-1023・1.f
値は float なら 0<e<255, double なら 0<e<2047 の時のものです. e=0の時は通常fも0となり, 全体で値は0を表します. e=0なのにf≠0の時は非正規数と呼ばれる数値で通常の演算では現れません. 3D Now!ではe=0の時は必ず0とみなされます. e=255, 1023の時は無限大か "数ではない"状態を表します. 3D Now!ではe=255の時の値は不定です.
なお, float使用時の最大数は2127〜1038, 1の付近での分解能は1/224〜6・10-8. 最大値を越えないように, また精度がそれほど高くないことに注意しましょう.
この格納形式を利用すると, 次のようなことが可能になります.
絶対値
最上位bitを0クリアします. fabs自体は全然遅くありませんが, もしメモリ上にあるfloatの配列に対して一気に絶対値をとりたい場合はFPU命令を使わずに整数レジスタだけで出来るということです.
正の数同士の比較
浮動小数の正の数値を符合なし整数とみなす時, 大きさの順序は保たれます. つまり整数レジスタの比較命令cmpが使えるわけです. fcom+fnstswによる比較では4〜6クロックかかるわけですから, これはうまく使えばかなり有効です.
最大値を得る関数のサンプル
御意見は光成滋生<herumi@nifty.com>までお願いします