固定小数点数

整数しか扱えないCPUで小数を扱うための、固定小数点数について説明します。

<注意>
このページでは、数式を表示するためにMathJaxを使用しています。ブラウザの設定でスクリプト実行をブロックしている場合、ページ内の数式が正しく表示されません。

固定小数点数とは

SNESのCPUである65C816には、小数を扱う命令がありません。そこで、整数の値を「ある決まった数で割ったもの」とみなすことで、小数を表現します。これを固定小数点数(fixed-point number)と言います。

定義 (固定小数点数)
整数\(\ N\ \)と、あらかじめ決めておいた非負整数\(\ f\ \)に対して、\(\ x = N / 2^f\ \)を、小数部\(\ f\ \)ビットの固定小数点数と言う。

2進数で考えると、これは「下から\(\ f\ \)ビット目と\(\ f+1\ \)ビット目の間に小数点がある」とみなしているのと同じです。小数点の位置が決まっていて動かないので、「固定」小数点数と呼ばれます。

たとえば\(\ f=8\ \)のとき、16ビットの値$0180は次の数を表します。

\[ \$0180 / 2^8 = 384 / 256 = 1.5 \]

上位バイト$01が整数部、下位バイト$80が小数部(\(\ \$80/256=0.5\ \))にあたります。小数部をちょうど1バイトにしておくと、整数部と小数部をバイト単位で取り出せるので、8ビットや16ビットのCPUではとても扱いやすくなります。

8.8形式の$0180のビットの重み

小数点より左のビットは\(\ 2^0=1\ \)、\(\ 2^1=2\ \)、…を、右のビットは\(\ 2^{-1}=0.5\ \)、\(\ 2^{-2}=0.25\ \)、…を表します。

表記法

固定小数点数の形式は、整数部と小数部のビット数を並べて書くのが一般的です。このサイトでは次のように書きます。

表示規則
整数部\(\ m\ \)ビット、小数部\(\ f\ \)ビットの固定小数点数を、\(\ m.f\ \)形式と書く。符号付きの場合、符号ビットは整数部に含める。

形式全体のビット数表せる範囲1単位の大きさ使用例
符号付き 8.816-128 ~ 127.996…1/256Mode 7の行列(M7A~M7D)
符号なし 2.12140 ~ 3.9997…1/4096DSPのピッチ
符号付き 16.824-32768 ~ 32767.996…1/256サブピクセル座標
符号付き 1.78-1 ~ 0.992…1/128FIRフィルタの係数、音量

一般に、符号付き\(\ m.f\ \)形式で表せる範囲は\(\ -2^{m-1}\ \)以上\(\ 2^{m-1}-2^{-f}\ \)以下、符号なしなら\(\ 0\ \)以上\(\ 2^m-2^{-f}\ \)以下です。負の数は、全体のビット列を2の補数で表します。たとえば符号付き8.8形式の\(\ -1.5\ \)は、\(\ 1.5=\$0180\ \)の2の補数で$FE80です。

DSPの音量やFIRフィルタの係数は「符号付き8ビットの値を128で割ったもの」として働くので、1.7形式の固定小数点数とみなせます。この形式では\(\ 1\ \)ちょうどを表すことができず、最大値$7Fは\(\ 127/128 \fallingdotseq 0.99\ \)倍になります。

加算と減算

小数部のビット数が同じ固定小数点数どうしは、ふつうの整数としてそのまま足したり引いたりできます。

\[ \frac{A}{2^f} + \frac{B}{2^f} = \frac{A+B}{2^f} \]

小数部のビット数が違う場合は、先にシフトして揃えてから計算します。たとえば8.8形式の値に整数\(\ n\ \)を足すには、\(\ n\ \)を8ビット左にシフトして(256倍して)から足します。

乗算

固定小数点数どうしを掛けると、小数部のビット数は2つの和になります。

\[ \frac{A}{2^f} \times \frac{B}{2^g} = \frac{AB}{2^{f+g}} \]

8.8形式どうしの積は16.16形式になるので、元の8.8形式に戻すには、積を8ビット右にシフトします。つまり、32ビットの積の、真ん中の2バイトを取り出すことになります。

例
\(\ 1.5 \times 2.25\ \)を8.8形式で計算する。
\(\ \$0180 \times \$0240 = 384 \times 576 = 221184 = \$036000\ \)
8ビット右にシフトして\(\ \$0360 = 864\ \)、すなわち\(\ 864/256=3.375\ \)。

一方を整数のまま掛ける場合は、結果の小数部のビット数は変わらないので、シフトは要りません。また、掛ける相手がちょうど\(\ 2^k\ \)なら、掛け算は\(\ k\ \)ビットの左シフトで済みます。

SNESには、8ビット×8ビットの符号なしの乗算器(WRMPYA・WRMPYB)と、Mode 7用の16ビット×8ビットの符号付きの乗算器(M7A・M7B → MPY)があります。使い方は掛け算と割り算を参照してください。

除算と逆数

固定小数点数どうしを割ると、小数部のビット数は2つの差になります。

\[ \frac{A/2^f}{B/2^g} = \frac{A}{B} \cdot 2^{g-f} \]

小数部のビット数が同じ値どうしを割ると、商は整数になり、小数部が失われます。小数部を残したいときは、割られる数を先に左シフトしておきます。8.8形式どうしで8.8形式の商を得るなら、\(\ A\ \)を8ビット左にシフトしてから\(\ B\ \)で割ります。

割り算は掛け算よりもずっと時間がかかります。同じ値で何度も割るときは、先に逆数\(\ 1/B\ \)を求めておき、掛け算に置き換えるのが定石です。

丸め

右シフトで小数部を捨てると、結果は常に小さいほうへ丸められます(切り捨て)。正の数では0に近づき、負の数では0から遠ざかります。たとえば\(\ -0.5\ \)(8.8形式で$FF80)の小数部を捨てると、\(\ \$FF=-1\ \)になります。

四捨五入にしたいときは、シフトする前に、捨てる桁のちょうど半分(8ビット右シフトなら$80)を足しておきます。

\[ \mathrm{round}(x) = \left\lfloor x + \frac{1}{2} \right\rfloor \]

切り捨てと四捨五入

階段が丸めた結果、破線が元の値です。
切り捨てでは、正の数も負の数も小さいほうへ丸められるので、\(\ -0.5\ \)は\(\ -1\ \)になります。

なお、65C816には符号を保ったまま右シフトする命令(算術右シフト)がありません。符号付きの値を右シフトするときは、最上位ビットをキャリーにコピーしてからROR命令を使います。

    CMP #$8000      ; 負の数ならキャリーが立つ
    ROR A           ; キャリー(符号)を最上位に入れながら右シフト

誤差

固定小数点数で表せるのは、\(\ 1/2^f\ \)の整数倍だけです。それ以外の値は、最も近い表せる値で近似されるので、最大で1単位の半分(\(\ 1/2^{f+1}\ \))の誤差が出ます。

この誤差は、計算を繰り返すと積み重なります。たとえば、毎フレーム速度に係数を掛けて減速させる処理では、丸めの向きによって、速度がいつまでも0にならなかったり、想定より早く0になったりします。小数部のビット数を増やすか、0付近を別に扱うなどの対策が必要です。

浮動小数点数との違い

パソコンなどで使われる浮動小数点数(floating-point number)は、値を「仮数×2の指数乗」の形で持ち、指数によって小数点の位置を動かします。

固定小数点数浮動小数点数
表せる範囲狭いとても広い
精度値の大きさによらず一定(絶対誤差が一定)値の大きさに比例(相対誤差が一定)
計算整数の命令だけでできる、速い専用の回路か、複雑な処理が必要

固定小数点数と浮動小数点数で表せる値の間隔

固定小数点数と浮動小数点数で表せる値を、数直線に並べたものです(分かりやすくするため、ビット数を少なくしています)。
固定小数点数は間隔が一定で、浮動小数点数は0に近いほど細かく、大きい値ほど粗くなります。

SNESのような整数しか扱えないCPUでは、浮動小数点数の計算はとても遅くなります。座標や速度、角度のように、扱う値の範囲があらかじめ決まっているものは、固定小数点数で十分です。

参照

2進法および16進法
合同算術
差分法と数値積分法
多項式近似