気まぐれにアセンブラの勉強~。
Delphiのインラインアセンブラなのは単にデバッグが楽だからなので気にしない。
呼びだし規約はstdcallに準ずる事にします。
function A_e: Extended; stdcall; const Value: Extended = 0.12345678901234567890123; asm fld Value end; function A_d: Double; stdcall; const Value: Double = 0.12345678901234567890123; asm fld Value end; function A_s: Single; stdcall; const Value: Single = 0.12345678901234567890123; asm fld Value end; begin WriteLn(FloatToStrF(A_e, ffFixed, 18, 18)); WriteLn(FloatToStrF(A_d, ffFixed, 18, 18)); WriteLn(FloatToStrF(A_s, ffFixed, 18, 18)); end.
0.123456789012345679 0.123456789012345677 0.123456791043281555
戻り値は、FPUのスタックというものに積んで返します。 FPUのスタックは、コントロールワードで決定された精度を持ちます。 Delphiの場合、何もしなくてもスタートアップルーチンがExtended精度に設定してくれます。 つまり、実は戻り値の型の精度はアセンブラ上では意味がありません。
メモリから値を取り出してスタックへ積むには、fldを使います。
結果の精度も見て下さい。 FloatToStrFは18桁まで表示可能なのですが、一番近い値を示しているのはExtendedです。
関係ないと書いたばっかですが、Delphiのインラインアセンブラ賢いですね~。
定数の型をちゃんとわかっていて、それぞれ tword ptr, qword ptr, dword ptr 扱いしてくれます。
function A_e(const A, B: Extended): Extended; stdcall; asm fld A fld B fadd end; function A_d(const A, B: Double): Double; stdcall; asm fld A fld B fadd end; function A_s(const A, B: Single): Single; stdcall; asm fld A fld B fadd end; begin WriteLn(FloatToStrF(A_e(PI, Exp(1)), ffFixed, 18, 18)); WriteLn(FloatToStrF(A_d(PI, Exp(1)), ffFixed, 18, 18)); WriteLn(FloatToStrF(A_s(PI, Exp(1)), ffFixed, 18, 18)); end.
5.859874482048838470 5.859874482048838210 5.859874486923217770
FPUのスタックは、スタックですから、値のロードを繰り返すと、順番に積まれて、最後に積んだのが一番上に来ます。
faddは、スタックの上二つの値を取り出して、足した結果を積みます。
function A_e(const A, B: Extended): Extended; stdcall; asm fld A fld B fsub end; begin WriteLn(FloatToStrF(A_e(PI, Exp(1)), ffFixed, 18, 18)); end.
0.423310825130748003
浮動小数点型(Extended, Double, Single)というのは、メモリとFPUのスタックの間でのやりとりでのみ意味を持ちますが、Delphiのインラインアセンブラは前述のように自動判別してくれます。 コードが記述上変わらないのに三通り書くのが馬鹿らしくなってきたので、以後理由が無ければExtendedのみに統一します。
fsubは、最後に積んだ値を、その前に積んだ値から引きます。
function A_e(const X: Extended): Extended; stdcall; asm fld X fchs end; begin WriteLn(FloatToStrF(A_e(1), ffFixed, 18, 18)); end.
-1.000000000000000000
fchsはスタックトップの符号を反転させます。
function A_e(const A, B: Extended): Extended; stdcall; asm fld A fld B fmul end; function B_e(const A, B: Extended): Extended; stdcall; asm fld A fld B fdiv end; begin WriteLn(FloatToStrF(A_e(2, Sqrt(2)), ffFixed, 18, 18)); WriteLn(FloatToStrF(B_e(2, Sqrt(2)), ffFixed, 18, 18)); end.
2.828427124746190100 1.414213562373095050
fmulがかけ算、fdivが割り算。
function Zero: Extended; stdcall; asm fldz end; function One: Extended; stdcall; asm fld1 end; function Pi: Extended; stdcall; asm fldpi end; function Log_10_2: Extended; stdcall; asm fldlg2 end; function Log_2_10: Extended; stdcall; asm fldl2t end; function Log_e_2: Extended; stdcall; asm fldln2 end; function Log_2_e: Extended; stdcall; asm fldl2e end; begin WriteLn(FloatToStrF(Zero, ffFixed, 18, 18)); WriteLn(FloatToStrF(One, ffFixed, 18, 18)); WriteLn(FloatToStrF(Pi, ffFixed, 18, 18)); WriteLn(FloatToStrF(Log_10_2, ffFixed, 18, 18)); WriteLn(FloatToStrF(Log_2_10, ffFixed, 18, 18)); WriteLn(FloatToStrF(Log_e_2, ffFixed, 18, 18)); WriteLn(FloatToStrF(Log_2_e, ffFixed, 18, 18)); end.
0.000000000000000000 1.000000000000000000 3.141592653589793240 0.301029995663981195 3.321928094887362350 0.693147180559945309 1.442695040888963410
よく使うような定数は、コード上に埋めこまなくても、使用可能です。 fldXXは、定数をスタックに積みます。 (fldpiとか見ると、C言語に限った話ではありませんが、#define M_PIの類が馬鹿らしく思えてきません?…機械語って偉大だ)
ここらで、具体的な式、例えば…適当ですけど、 1 / X + 2 * Y を計算してみます。
function A_e(const X, Y: Extended): Extended; stdcall; const _2: Extended = 2; asm fld1 fld X fdiv fld _2 fld Y fmul fadd end; begin WriteLn(FloatToStrF(A_e(2, Pi), ffFixed, 18, 18)); end.
6.783185307179586480
FPUはスタックを用いた計算を行ないますので、まず 1 / X + 2 * Y を逆ポーランド記法に直します。 すると 1 X / 2 Y * + 。後はこの通りに命令を並べるだけです。
逆ポーランド記法がわからない人は、拙作「いい電卓」で普通の式から変換できます。(宣伝)
var R: Extended; procedure A_e; stdcall; asm fld1 fstp R end; begin A_e; WriteLn(FloatToStrF(R, ffFixed, 18, 18)); end.
1.000000000000000000
fstpでスタックから値をポップしてメモリ上に代入できます。
ここではDelphiのインラインアセンブラだから型を省略できていますが、普通はtword ptr, qword ptr, dword ptrを書かなければなりません。
function IntToFloat(X: Integer): Extended; stdcall; asm fild X end; function UnsignedToFloat(X: Cardinal): Extended; stdcall; var Work: Int64; asm mov Work.Int64Rec.Hi, 0 mov eax, X mov Work.Int64Rec.Lo, eax fild Work end; begin WriteLn(FloatToStrF(IntToFloat(-1), ffFixed, 18, 18)); WriteLn(FloatToStrF(UnsignedToFloat($ffffffff), ffFixed, 18, 18)); end.
-1.000000000000000000 4294967295.000000000000000000
fildで整数をスタックに積めますが、この命令は、常に符号付きの扱いをしてくれます。 なので、符号無し整数の場合は、一旦、一回り大きな整数型を経由する必要があります。
function Round(const X: Extended): Integer; stdcall; var R: Integer; asm fld X fistp R mov eax, R end; function Trunc(const X: Extended): Integer; stdcall; var SaveCW, WorkCW: Word; R: Integer; asm fld X fnstcw SaveCW fnstcw WorkCW or WorkCW, $0c00 fldcw WorkCW fistp R fldcw SaveCW mov eax, R end; function Ceil(const X: Extended): Integer; stdcall; var SaveCW, WorkCW: Word; R: Integer; asm fld X fnstcw SaveCW fnstcw WorkCW and WorkCW, $f3ff or WorkCW, $0800 fldcw WorkCW fistp R fldcw SaveCW mov eax, R end; function Floor(const X: Extended): Integer; stdcall; var SaveCW, WorkCW: Word; R: Integer; asm fld X fnstcw SaveCW fnstcw WorkCW and WorkCW, $f3ff or WorkCW, $0400 fldcw WorkCW fistp R fldcw SaveCW mov eax, R end; begin WriteLn(Round(1.7), '/', Round(-1.7), '/', Round(1.3), '/', Round(-1.3)); WriteLn(Trunc(1.7), '/', Trunc(-1.7), '/', Trunc(1.3), '/', Trunc(-1.3)); WriteLn(Ceil(1.7), '/', Ceil(-1.7), '/', Ceil(1.3), '/', Ceil(-1.3)); WriteLn(Floor(1.7), '/', Floor(-1.7), '/', Floor(1.3), '/', Floor(-1.3)); end.
2/-2/1/-1 1/-1/1/-1 2/-1/2/-1 1/-2/1/-2
fistpでスタックから値を取り出し整数としてメモリへ書き出せますが、小数点以下の扱い方が、銀行家の丸め、ゼロ方向へ切り捨て、+∞方向へ切り上げ、-∞方向へ切り下げ、の4種類あります。
デフォルトが銀行家の丸めなので、それ以外は、FPUのコントロールワードを一時的に書き換えてやります。
銀行家の丸めとは、原則四捨五入ですが、丁度0.5の時は、偶数側を選ぶ…らしいです。
Delphiのヘルプには「JIS ハンドブック標準化「数値の丸め方」(Z8401-1961)を参照してください」なんて書いてありますが…。
普通の四捨五入が欲しい時は、当たり前ですが、0.5を足して切り捨てでおっけーです。
コントロールワードについては、僕もよくわかって無いので詳しい説明は省かせて下さい。 fnstcwで取得、fldcwで設定です。
function G(const X, Y: Extended): Boolean; stdcall; asm fld Y fld X fcompp fnstsw ax sahf seta al end; begin WriteLn(G(1, 2), '/', G(2, 1)); end;
FALSE/TRUE
fcomppで比較を行ないますが、比較結果がFPUのステータスワードという特別な場所に格納されるため、fstsw axでステータスワードをaxに取得、sahfでax中の比較結果をフラグレジスタに映します。
後は、通常のja等で分岐したり、seta等で結果を得たりできるという寸法です。
a, b, ae, be, e, ne系は動作を確認しましたが、g, ge, l, leは上手く行きませんでした。
ですので、前者を使っておけば問題無い様です。
後から積んだ方が、比較の基準になるみたいなので、注意。ここだけ今までの感覚と逆なんですね。
function Sin(const X: Extended): Extended; stdcall; asm fld X fsin end; function Cos(const X: Extended): Extended; stdcall; asm fld X fcos end; function Tan(const X: Extended): Extended; stdcall; asm fld X fptan fstp st(0) end; function ArcTan(const Y: Extended): Extended; asm fld Y fld1 fpatan end; function ArcTan2(const Y, X: Extended): Extended; asm fld Y fld X fpatan end; function Abs(const X: Extended): Extended; stdcall; asm fld X fabs end; function Sqrt(const X: Extended): Extended; stdcall; asm fld X fsqrt end; function Log(const Base, X: Extended): Extended; stdcall; asm fld1 fld X fyl2x fld1 fld Base fyl2x fdiv end; function E: Extended; stdcall; asm fld1 { 2 ^ (log 2 e) } fldl2e fld1 fsub f2xm1 fld1 fadd fscale fstp st(1) end; function Log2(const X: Extended): Extended; stdcall; asm fld1 fld X fyl2x end; function Pow2(const X: Extended): Extended; stdcall; asm fld X fld1 fscale fstp st(1) end; begin WriteLn(FloatToStrF(Sin(Pi / 6), ffFixed, 18, 18)); WriteLn(FloatToStrF(Cos(Pi / 6), ffFixed, 18, 18)); WriteLn(FloatToStrF(Tan(Pi / 6), ffFixed, 18, 18)); WriteLn(FloatToStrF(ArcTan(1), ffFixed, 18, 18)); WriteLn(FloatToStrF(ArcTan2(1, Sqrt(3)), ffFixed, 18, 18)); WriteLn(FloatToStrF(Abs(-2), ffFixed, 18, 18)); WriteLn(FloatToStrF(Sqrt(2), ffFixed, 18, 18)); WriteLn(FloatToStrF(Log(2, 8), ffFixed, 18, 18)); WriteLn(FloatToStrF(E, ffFixed, 18, 18)); WriteLn(FloatToStrF(Pow2(Log2(2) * 3), ffFixed, 18, 18)); end.
0.500000000000000000 0.866025403784438647 0.577350269189625764 0.785398163397448310 0.523598775598298873 2.000000000000000000 1.414213562373095050 3.000000000000000000 2.718281828459045240 8.000000000000000000
eを得るやつだけ、やけに苦労していますが、2 ^ X - 1を計算するf2xm1が、Xの小数部分しか見てくれない(X > 1の時何もしない)のが原因です。
fscaleは、スタックトップに、2 ^ (スタックのひとつ前に積まれた数) を乗算します。単に2をかけ算しても同じですが、2を用意する方が面倒(定数にしてメモリから読むか、fld1, fld1, faddか)なので、こんな感じです。
fscaleはスタックのひとつ前に積まれた数を捨ててくれないので、fstp st(1)で捨ててやります。
要するに、実際には (2 ^ ((log 2 e) - 1) - 1 + 1) * 2 ^ 1 を計算しています。
Tanでも使っていますが、fstpは、fstp st(n)と書くと、スタックのn番目の値を捨てる事ができます。
これぐらいの基本的な関数があれば、後は組み合わせです。 例えば、最後の例では、Log 2 X を求める方法と、2 ^ Xを求める方法を組み合わせ、2 ^ 3 を計算しています。
2002-10-13 | 初稿 |
2002-10-17 | fmul, fdiv, 定数、式 |
2002-10-18 | 代入、fchs、整数との変換、各種関数 |