固定小数点数
整数しか扱えない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ではとても扱いやすくなります。

小数点より左のビットは\(\ 2^0=1\ \)、\(\ 2^1=2\ \)、…を、右のビットは\(\ 2^{-1}=0.5\ \)、\(\ 2^{-2}=0.25\ \)、…を表します。
表記法
固定小数点数の形式は、整数部と小数部のビット数を並べて書くのが一般的です。このサイトでは次のように書きます。
表示規則
整数部\(\ m\ \)ビット、小数部\(\ f\ \)ビットの固定小数点数を、\(\ m.f\ \)形式と書く。符号付きの場合、符号ビットは整数部に含める。
| 形式 | 全体のビット数 | 表せる範囲 | 1単位の大きさ | 使用例 |
|---|---|---|---|---|
| 符号付き 8.8 | 16 | -128 ~ 127.996… | 1/256 | Mode 7の行列(M7A~M7D) |
| 符号なし 2.12 | 14 | 0 ~ 3.9997… | 1/4096 | DSPのピッチ |
| 符号付き 16.8 | 24 | -32768 ~ 32767.996… | 1/256 | サブピクセル座標 |
| 符号付き 1.7 | 8 | -1 ~ 0.992… | 1/128 | FIRフィルタの係数、音量 |
一般に、符号付き\(\ 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では、浮動小数点数の計算はとても遅くなります。座標や速度、角度のように、扱う値の範囲があらかじめ決まっているものは、固定小数点数で十分です。