11. 浮動小数点演算 その1
今回は Linux に限定した話題ではありませんが,浮動小数点数の取り扱いに 関しての解説です.486DX より上位の CPU はすべて CPU に内蔵された浮動小数点 ユニット (以下 FPU) で有効数字 18 桁で 10の±4931乗の範囲の数値の演算が可能と なっています.せっかく用意されているのでアセンブラから FPU を直接使って 80 bit の浮動小数点演算をしてみます. (C の double は 64 bit, 勝った! :-)
x86系の CPU では,浮動小数点演算の実行はレジスタ中心の整数演算部分 とは違ってスタック中心のアーキテクチャを採用しています.浮動小数点 演算専用のスタックに数値を設定した後に演算命令を実行する方法を取ります. forth 言語を知っていると楽ですね ;-)
FPUのレジスタ構造
486DX より上位の CPU は浮動小数点ユニットとして以下のレジスタ群を持っています.
いくつものレジスタがありますが, 重要なレジスタは最初にある浮動小数点 データレジスタスタックです. 浮動小数点演算を行うために 80bit のレジスタを 8 本持っています. データレジスタは st0, st1,.. st7 のように 指定しますが, 整数型の命令と異なって, これらのレジスタはスタックを構成し, スタックポインタに相当する Status Word レジスタの bit 11-13 が指すレジスタ をスタックトップとして, そこから相対的に指定することになります. NASM ではスタックトップのレジスタを st0, 1つ下(古い)レジスタを st1 として指定します.
Status Word, Control Word, Tag Word レジスタの各ビットの意味を簡単に示しますが, あまり気にする必要はないでしょう.
- Status Word レジスタ
B ビジーフラグ C3 条件コード3 (ZF) ST スタックトップポインタ C2 条件コード2 (PF) C1 条件コード1 C0 条件コード0 (CF) ES エラーステータス SF スタックフォルト PE 精度例外フラグ UE アンダーフロー例外フラグ OE オーバーフロー例外フラグ ZE ゼロ除算例外フラグ DE デノーマル演算例外フラグ IE 無効演算例外フラグ
- Control Word レジスタ
RC 丸めモード 00:至近値(偶数) 01:近-∞ 10:近+∞ 11:切捨て PC 仮数精度制御 00:24bit 01:予約 10:53bit 11:64bit PM 精度例外マスク UM アンダーフロー例外マスク OM オーバーフロー例外マスク ZM ゼロ除算例外マスク DM デノーマル演算例外マスク IM 無効演算例外マスク
- Tag Word レジスタ
- データレジスタの状態を保持しています. 物理レジスタ番号に対応しています.
値 意味 00 有効値 01 ゼロ 10 特殊数(無限大, デノーマル, 非数) 11 空
詳細は Intel のオンラインマニュアル(日本語)を参照して下さい.
演算とデータレジスタスタック
FPU はデータレジスタスタックに積んだ数値に対して演算を行います. 次の図の (a) (b) は 16 - 4 = 12 の計算をする場合のデータレジスタスタック の様子を示しています.(c) は 16 と 4 を比較する場合の例です.
実行しているのは次のコードです.fld でメモリにある80ビットの浮動小数を スタックにつみます. (a) の fsub 命令でスタックの上部の2つの数値を取り出して 減算し, 結果をスタックに戻します. (b) の fsubp 命令では fsub の後にスタック をポップ(スタックトップを空にした後,ポインタをインクリメント)します. (c) の例では, fcompp で大きさを比較した結果を C0, C2, C3 の条件コードに設定 してスタックを2回ポップしています. 比較に使った数値が不用の場合です.
Num16 dt 16.0 Num4 dt 4.0 fld tword[Num16] fld tword[Num4] fsub st1, st0 ; (a) ; または fsubp st1,st0 (b) ; または fcompp st1,st0 (c)
使いやすいのは (b) の方法です. 直前の演算結果を次の処理で利用できるためです. FPU で「スタックのポップ」は「数値の廃棄」に相当することに注意して下さい.
fsub 命令の代わりに fadd なら減算, fmul なら乗算, fdiv なら除算ができます. fsin のような三角関数, 平方根を求める fsqrt などの関数はスタックトップに設定した引数が関数値に置き換わります.
浮動小数の入出力
浮動小数点演算自体は簡単ですが,難しい部分は浮動小数の入出力の部分です.
浮動小数点専用の命令を持っているため計算自体は非常に簡単です. しかし, 文字として入力された浮動小数点数を演算に使用できる形式に変換したり, 演算結果を (文字として) 表示するのは,計算に比べて非常に面倒です. 演算は内部的に 80 ビットの2進数値で行われるため,数値は80 ビットの 2進数値で与え,結果も 80 ビットの 2進数値で返ります.
class="verbatim-code"実数を表す文字列と 80 ビットの浮動小数点数の2進表現との相互変換をする ためのサブルーチンを用意します.
FPU の 80bit の 2進浮動小数点数で表現された内部データを表示のために 10進数に変換する部分と文字列で表現された浮動小数点数を FPU の 80bit 2進浮動小数点数に変換するためのプログラムです.
;--------------------------------------------------------------------- ; Floating Point ; file : float.inc ; 2000/11/04 (C) Jun Mizutani ;--------------------------------------------------------------------- %ifndef __FLOAT_INC %define __FLOAT_INC section .bss Sign resb 1 ; 符号 BCD rest 1 ; 仮数値 BCD 表現(10バイト) ExpSign resb 1 ; 指数部符号 Exponent resd 1 ; 指数値(4バイト) DigitStr resb 20 ; BCD変換用数字列 buffer section .text ;--------------------------------------------------------------------- ; FPU のスタックトップのレジスタ中の浮動小数点数をバッファに文字列 ; として格納する. データレジスタスタックは pop されるため, 必要なら ; call 前に fld st0 で dup しておくこと. ; BCDはリトルエンディアンで格納されていることに注意 ; レジスタは保存される ; edi に文字列用のバッファ先頭アドレスを指定して呼ぶ (29byte以上) ;--------------------------------------------------------------------- Float2Ascii: pusha call Float2BCD ; 浮動小数点数 ffree st0 ; スタックトップを空に fincstp ; pop cmp byte[Sign], 0 ; 符号をチェック je .positive mov al, "-" mov [edi], al inc edi .positive mov esi, BCD add esi, 8 ; BCD先頭無効バイトをスキップ mov bh, [esi] mov bl, bh shr bh, 4 or bh, 0x30 mov [edi], bh ; 小数点以下第1桁 inc edi mov al, "." ; 小数点 mov [edi], al inc edi and bl, 0x0F or bl, 0x30 mov [edi], bl ; 第1桁 inc edi .next dec esi ; 次のBCD1バイトを取得 mov bh, [esi] mov bl, bh shr bh, 4 or bh, 0x30 mov [edi], bh inc edi and bl, 0x0F or bl, 0x30 mov [edi], bl inc edi cmp esi, BCD jne .next ; 指数部の表示 mov ebx, [Exponent] or ebx, ebx ; 指数値をチェック je .exit mov al, "e" mov [edi], al inc edi or ebx, ebx ; 指数値をチェック jg .e_positive neg ebx mov al, "-" mov [edi], al inc edi .e_positive ; 指数値を数字に変換 mov eax, ebx mov ebx, 100000000 mov ecx, 9 mov esi, 0 .exp_loop: xor edx, edx ; edx = 0 div ebx ; eax = edx:eax / ebx push edx ; modulo test eax, eax jz .ps1 add al, '0' mov [edi], al inc edi inc esi ; 0 以外の数値出現 jmp short .ps2 .ps1: test esi, esi ; 前の 0 は表示しない jz .ps2 add al, '0' ; 10/29 mov [edi], al inc edi .ps2: pop eax ; modulo push edi xor edx, edx ; edx = 0 xchg eax, ebx ; : save eax mov edi, 10 ; ebx = ebx / 10 div edi ; : xchg eax, ebx ; : restore eax pop edi loop .exp_loop .exit xor eax, eax mov [edi], al popa ret ;--------------------------------------------------------------------- ; FPU のスタックトップのレジスタ中の浮動小数点数を ; Sign, BCD, Exponent に分解して格納 ; 2進表現の 80bit 浮動小数点数を10進表現に変換 ; Float2Ascii から呼び出される ;--------------------------------------------------------------------- Float2BCD: push eax mov byte [Sign], 0 ; 正の数値を仮定 fldz ; 0 をプッシュ fcomp st1 ; st0 と st1 を比較して pop fstsw ax ; フラグをaxに転送 sahf ; ah をフラグレジスタに転送 jz near .zero push ebx push edx .notzero ; ゼロでなければ, 符号を設定 jb .Positive fabs ; 符号を記録し,絶対値を求める mov byte [Sign], 1 .Positive ; 符号が正 mov dword [Exponent], 0 ; 指数を 0 で初期化 ; 指数の正負を判定 fld1 ; 1.0 をプッシュ fcomp st1 ; (1.0 - n) 判定が逆になる fstsw ax sahf jbe .PosExp ; 1以下なら.NegExp ;---------------------------------------------------------- ; 負の指数の場合 ; ここで 0 から 1 の間の数値が得られている. ; 適当な10のべき乗を乗算して数値fが 1 < f < 10 となるようにする. mov ebx, TblSize*10 ; 表の直後に設定 mov edx, 8192 .NegExp sub ebx, 10 ; 表の1つ前に移動 shr edx, 1 test edx, edx ; べき数が0なら次へ jz .mul10 fld tword [NegTbl + ebx]; 10の負のべき乗 fcomp st1 ; st0(10^n) - st1 fstsw ax ; フラグをaxに転送 sahf ; ah をフラグレジスタに転送 jbe .NegExp ; 小さい間繰り返し 11/1 sub [Exponent], edx ; 指数 -= ExpTbl [di] fld tword [PosTbl + ebx]; 10の正のべき乗を乗算 fmulp st1, st0 jmp .NegExp ; さらに繰り返し .mul10 fld tword [PosTbl] ; 10を乗算 fmulp st1, st0 dec dword [Exponent] ; 指数 -= 1 jmp .range1_10 ;---------------------------------------------------------- ; 正の指数の数値を扱う ; 適当な10のべき乗を除算して数値fが 1< f <10 となるようにする. .PosExp mov ebx, TblSize*10 ; 表を後ろから参照 mov edx, 8192 .PosExp2 sub ebx, 10 ; 表の1つ前に移動 shr edx, 1 test edx, edx jz .range1_10 fld tword [PosTbl + ebx]; 10の正のべき乗 fcomp st1 ; st0(10^n) - st1 fstsw ax ; フラグをaxに転送 sahf ; ah をフラグレジスタに転送 ja .PosExp2 ; 大きい間繰り返し add [Exponent], edx fld tword [PosTbl + ebx]; 1 x 2^n fdivp st1, st0 ; 除算 st1/st0 jmp .PosExp2 ; さらに繰り返し .range1_10 ; この時点で数値は 1 <= x < 10 の範囲になっている, ; 10^17 を乗じて(残りの17桁分)整数化し,BCDに変換する fld tword [MaxDigits] fmul st1 ; 10^18で fbstp に失敗する場合に対処 fld tword [MaxBCD] fcomp st1 fstsw ax ; フラグをaxに転送 sahf ; ah をフラグレジスタに転送 jae .bcd fld tword [PosTbl] fdivp st1, st0 inc dword [Exponent] ; 指数 += 1 .bcd fbstp tword [BCD] ; stack topをBCD18桁でメモリ10byteに格納 ; 変換前にRCで指定される丸めモードで整数化 ; BCD には 18桁のBCDとして返る. pop edx pop ebx pop eax ret ; ゼロの場合は特別,定数としてのゼロを設定 .zero xor eax, eax mov [BCD], eax ; BCDの10バイトを0に設定 mov [BCD + 4], eax mov [BCD + 8], ax mov dword[Exponent], 0 mov byte[Sign], ' ' pop eax ret ;--------------------------------------------------------------------- ; edi に設定された浮動小数点数の文字列を 80bit で FPU のスタックトップ ; データレジスタに設定する.先頭部分の空白やタブは除いておく. ; 数値が "yyy.xxxx" (y <> 0) の形式のとき ; "yyy" の桁が18桁以上の場合は単に指数を増加させて数字は無視 ; このコードは文字を DigitStr 配列に逆順に格納していることに注意. ; リトルエンディアンの場合この方が有利 ; 10進表現の浮動小数文字列を 2進表現 80bit 浮動小数に変換 ; edi は読み込み済みのバッファの次の位置が返る. 他のレジスタは保存. ;--------------------------------------------------------------------- Ascii2Float: push eax push ebx push edx xor eax, eax mov ebx, DigitStr .Clear mov [ebx], eax add ebx, 4 cmp ebx, DigitStr + 20 jne .Clear mov edx, -18 ; BCD変換のための指数の初期値 mov ebx, 18 ; 処理すべき残っている有効桁数 mov byte[Sign], 0 call GetChar jae .SkipZero je .FractionPart cmp al, "+" ; 符号付? je .SkipSign cmp al, "-" jne near .ConvErr mov byte[Sign], 1 .SkipSign: call GetChar jae .SkipZero cmp al, "." ; 小数点から始まる? je .FractionPart jmp near .ConvErr .SkipZero cmp al, 0 ; 先頭部分の 0 を除く jne .Digits call GetChar jb .DecimalPoint jmp .SkipZero .Digits: inc edx ; 小数点以上の数値桁数を調整 test ebx, ebx ; 有効桁数を超えていないか jz .SkipDigit ; 18桁を越えたら数字はスキップ mov [DigitStr + ebx], al; 後ろから詰める dec ebx .SkipDigit call GetChar jb .DecimalPoint jmp .Digits .DecimalPoint: cmp al, "." jne .BCDConv ;---------------------------------------------------------- ; 小数点以下の桁の処理 ; 小数点以下の各桁では1桁ごとに指数を減少させる必要がある. ; FPU の BCD変換では小数点以下の値は許されないため. ; 例えば 0.12345 --> 12345e-5 とする. .FractionPart: call GetChar jb .BCDConv cmp ebx, 18 ; すでに0以外の数値 10/29 jne .Fp2 ; 10/29 cmp al, 0 ; .0000xx をチェック jne .Fp2 dec edx jmp .FractionPart .FractionPart2: call GetChar jb .BCDConv .Fp2: test ebx, ebx ; 有効桁数を超えたか? jz .FractionPart2 ; 超えていたら無視する mov [DigitStr + ebx], al; 超えていなければ格納 dec ebx jmp .FractionPart2 ;---------------------------------------------------------- ; DigitStr 変数を BCD に変換 ; 数字の文字列の数字2つの下位4ビットからBCDの1バイトに変換 .BCDConv: push eax push edi push esi mov edi, BCD add edi, 8 mov esi, DigitStr add esi, 18 .BCDLoop: mov al, [esi] dec esi shl al,4 or al, [esi] dec esi mov [edi], al dec edi cmp esi, DigitStr ja .BCDLoop fbld [BCD] ; BCD の値を FPU にロード pop esi pop edi pop eax ;---------------------------------------------------------- ; 指数部の処理 .ExponentPart cmp al, "e" ; e または E のチェック je .Exponent cmp al, "E" jne .SetExponent ;---------------------------------------------------------- ; 指数の値を読んで BCD用に調整された指数値に加える .Exponent mov byte[ExpSign], 0 ; 正の指数を仮定 xor ebx, ebx xor eax, eax call GetChar ; E の後ろの文字取得 jae .ExpNum ; 正の指数 cmp al, "+" je .EatExpSign cmp al, "-" jne near .ConvErr ; +,-,Digit でない mov byte[ExpSign], 1 ; 指数は負 .EatExpSign: call GetChar ; 符号の後ろの文字取得 jb near .ConvErr ; 符号の後ろに数字必要 .ExpNum: imul ebx, 10 ; 指数部の数字を数値化 add ebx, eax call GetChar jae .ExpNum cmp byte[ExpSign], 0 ; 指数は負か? je .PosExp neg ebx ; 負にする .PosExp: add edx, ebx ; BCD化用の指数に加える .SetExponent cmp edx, 4930 ; 指数が範囲外かチェック jg .ConvErr cmp edx, -4930 jl .ConvErr mov ebx, TblSize*10 ; 表の直後に設定 test edx, edx ; 指数が負かどうか js .NegativeExp ;---------------------------------------------------------- ; 指数部は正 shl edx, 19 ; キャリーを使う .ExpLoop1: test edx, edx ; 指数が0なら終了 jz .Done sub ebx, 10 shl edx, 1 jnc .ExpLoop1 fld tword[PosTbl + ebx] fmulp st1, st0 jmp .ExpLoop1 ;---------------------------------------------------------- ; 指数部は負 .NegativeExp: neg edx ; 絶対値を求める shl edx, 19 ; キャリーを使う .ExpLoop2: test edx, edx ; 0 なら終了 jz .Done sub ebx, 10 shl edx, 1 jnc .ExpLoop2 fld tword[PosTbl + ebx] fdivp st1, st0 jmp .ExpLoop2 .Done: cmp byte[Sign], 0 je .Exit fchs .Exit pop edx pop ebx pop eax clc ret .ConvErr: pop edx pop ebx pop eax stc ret ;--------------------------------------------------------------------- ; 2進10進変換用の定数 ;--------------------------------------------------------------------- MaxBCD: dt 9.99999999999999999e17 MaxDigits dt 1.0e+17 NegTbl dt 1.0e-1 dt 1.0e-2 dt 1.0e-4 dt 1.0e-8 dt 1.0e-16 dt 1.0e-32 dt 1.0e-64 dt 1.0e-128 dt 1.0e-256 dt 1.0e-512 dt 1.0e-1024 dt 1.0e-2048 dt 1.0e-4096 PosTbl: dt 10.0 dt 1.0e+2 dt 1.0e+4 dt 1.0e+8 dt 1.0e+16 dt 1.0e+32 dt 1.0e+64 dt 1.0e+128 dt 1.0e+256 dt 1.0e+512 dt 1.0e+1024 dt 1.0e+2048 dt 1.0e+4096 TblSize equ ($-PosTbl)/10 ;--------------------------------------------------------------------- ; AL の文字が数字かどうかのチェック ; 数字なら整数に変換して AL 返す. 非数字ならキャリーセット ;--------------------------------------------------------------------- IsDigit: cmp al, "0" ; 数字か? jb NotDigit cmp al, "9" ja NotDigit sub al, "0" ; 整数に変換 ctc ret NotDigit: stc ret ;--------------------------------------------------------------------- ; バッファの次の数字を AL に取得 ; 数字なら整数に変換して AL 返す. 非数字ならキャリーセット ;--------------------------------------------------------------------- GetChar: mov al, [edi] inc edi ; 次の文字を AL に取得 call IsDigit ret %endif
結構複雑ですが, 興味のある方はコメントをたよりにコードを追って見て下さい.
使うだけなら覚えることは:
- edi に浮動小数点数の文字列の先頭アドレスを設定して call Ascii2Float
- edi に 29 バイト以上のバッファの先頭アドレスを設定して call Float2Ascii
です.
使用例
float.inc を使った簡単な例です.100πを計算し,文字列から数値への変換をテスト しています.
;--------------------------------------------------------------------- ; testfpu.asm ; 2000/10/03 Jun Mizutani ;--------------------------------------------------------------------- %include "stdio.inc" %include "float.inc" section .bss fstring resb 32 section .data Num100 dt 100.0 TestNum0 db "000012345678901234567890123.456",0 TestNum1 db "0000.0000000000012345678901234567890123",0 TestNum2 db ".123456789012345678E-7",0 cset db ' Conversion error!', 0x0A, 0 cclear db ' OK!', 0x0A, 0 section .text global _start ;------------------------------------ ; start here ;------------------------------------ _start: finit ; FPU を初期化 fldpi ; πをpush fld tword [Num100] ; 100をpush fmulp st1, st0 ; 100π call print_float ; 表示 mov edi, TestNum0 call print_atof ; 文字列を浮動小数に変換表示 mov edi, TestNum1 call print_atof ; 文字列を浮動小数に変換表示 mov edi, TestNum2 call Exit ;------------------------------------ ; ediを先頭とする文字列を浮動小数に変換して表示 print_atof mov eax, edi call OutAsciiZ call Ascii2Float ; 浮動小数文字列をFPUにセット jb .error mov eax, cclear ; 変換失敗 jmp .cend .error mov eax, cset ; 変換エラー .cend call OutAsciiZ ; FPU スタックトップを表示 print_float mov edi, fstring call Float2Ascii mov eax, fstring call OutAsciiZ call NewLine call NewLine ret
実行結果
jm:~/ex_asm$ ./asm testfpu jm:~/ex_asm$ ./testfpu 3.14159265358979324e2 000012345678901234567890123.456 OK! 1.23456789012345678e22 0000.0000000000012345678901234567890123 OK! 1.23456789012345678e-12
浮動小数点の計算は最近の CPU にとっては簡単な仕事です.