固定小数点数について
何回かに渡り初等関数の計算について取り上げてきました。本ブログのテーマが組み込みである事を差し置いて変数の型にdoubleを使用していましたが、実際に組み込みにおいてdoubleを使用するには、対象マイコンのアーキテクチャにFPU(Floating point number Processing Unit)が含まれているかを確認した方が良いかと思います。(例えばRXマイコンであればFPUが搭載されていますがfloatと限定的です。その場合はあまり誤差が問題になることはないと思いますのでdoubleをfloatと読み替えてください。) 整数型の演算は僅かなCPUサイクルで実現できるようハードウェアに除算器、乗算器が搭載されていますが、浮動小数点の演算はFPUがないと多くのCPUサイクルを消費してしまいます。そのため知らないでdoubleを使用するとシステムのパフォーマンスを悪化させかねません。
さてFPUがない上に小数点を扱わなければいけないといった時どうすればよいでしょうか?その場合は固定小数点数を使います。一般的に固定小数点数の演算の方が浮動小数点数の演算より早いため精度を多少犠牲にするものの多くのDSPではいまだ固定小数点数を使用しています。
固定小数点数の型は標準では準備されていません、自分で準備しなければいけませんが、ここでは32bitで固定小数点数を表してみたいと思います。小数点以下は任意ですのでここでは16bitとします。
| 31 | 30 | 29 | 28 | 27 | ~ | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | ~ | 3 | 2 | 1 | 0 |
| sign | decimal | fraction | ||||||||||||||||
最上位ビットをsignにしていますが実際のマイナスの表現は2の補数(ビット反転して1足した数)を使用したいと思います。この型を以下のように準備してみました。ビットフィールドなのでエンディアンがシステム依存になりますが、よしとさせて下さい。
typedef union UnFix16_{
signed int iNum;
struct StFix16{
unsigned int fraction: 16;
unsigned int decimal : 15;
unsigned int sign : 1;
} stFix16;
}UnFix16;
ここでunionでint型とstructを共存させているところがポイントとなります。なお似たようにfloat型の中身にアクセスする型は次のようになります。
typedef union UnFloat_{
signed int iNum;
struct StFloat{
unsigned int fraction: 23;
unsigned int exponent : 8;
unsigned int sign : 1;
} stFloat;
}UnFloat;
型は準備できましたので次に変換や代入、演算を準備していきましょう。関数の引数としてこれらを準備しても良いのですが、クラス表現にしてみます。
#include <iostream>
class CFix16{
public:
CFix16(){};
CFix16(float f_Fix) : m_uF(fromfloat(f_Fix)){}
virtual ~CFix16(){}
UnFix16 fromfloat(float fL);
float CFix16::tofloat(void);
inline UnFix16 const get(void){ return m_uF; }
/* 複写 */
const CFix16 &operator=( const float &fn )
{
m_uF = fromfloat(fn);
return *this;
}
const CFix16 &operator=( const UnFix16 &uF )
{
m_uF = uF;
return *this;
}
const CFix16 &operator=( CFix16 &cn )
{
m_uF = cn.get();
return *this;
}
/* 比較 */
CFix16 operator==( const float &fn )
{
if( m_uF.iNum == fromfloat(fn).iNum ){
return 1;
}else{
return 0;
}
}
CFix16 operator==( CFix16 &cF )
{
if( m_uF.iNum == cF.get().iNum ){
return 1;
}else{
return 0;
}
}
/* 演算 */
UnFix16 operator+( CFix16 &cF )
{
UnFix16 uF;
uF.iNum=m_uF.iNum + cF.get().iNum;
return( uF );
}
UnFix16 operator-( CFix16 &cF )
{
UnFix16 uF;
uF.iNum=m_uF.iNum - cF.get().iNum;
return( uF );
}
UnFix16 operator*( CFix16 &cF )
{
UnFix16 uF;
uF.iNum = ((__int64)m_uF.iNum * (__int64)cF.get().iNum) >> 16 ;
return( uF );
}
UnFix16 operator/( CFix16 &cF )
{
UnFix16 uF;
uF.iNum = (((__int64)m_uF.iNum << 32) / cF.get().iNum) >> 16 ;
return( uF );
}
private:
UnFix16 m_uF;
};
UnFix16 CFix16::fromfloat(float fL)
{
UnFix16 uF;
UnFloat *uFl = (UnFloat*)&fL;
uF.iNum = 0;
if( uFl->stFloat.sign ){
uFl->stFloat.sign = 0;
uF.stFix16.decimal = (unsigned int)fL;
fL = ( fL - (unsigned int)fL + 1.0);
uF.stFix16.fraction |= uFl->iNum >> 7;
uF.iNum = ( ~uF.iNum + 1);
} else {
uF.stFix16.decimal = (unsigned int)fL;
fL = ( fL - (unsigned int)fL + 1.0);
uF.stFix16.fraction |= uFl->iNum >> 7;
}
return uF;
}
float CFix16::tofloat(void)
{
UnFix16 uF = m_uF;
float fL;
UnFloat *uFl = (UnFloat*)&fL;
uFl->iNum = 0;
uFl->stFloat.exponent = 127;
if( uF.stFix16.sign ){
uF.iNum = ~( uF.iNum - 1 );
uFl->stFloat.fraction |= uF.stFix16.fraction << 7;
fL = fL + (float)uF.stFix16.decimal - 1.0;
uFl->stFloat.sign = 1;
}else{
uFl->stFloat.fraction |= uF.stFix16.fraction << 7;
fL = fL + (float)uF.stFix16.decimal - 1.0;
}
std::cout << fL << std::endl;
return fL;
}
floatから固定小数点数、固定小数点数からfloatへの変換ですが 直観的に小数点を扱えるようにするための施策であって、実際のプログラム上では使用しないようにします。固定小数点数の強みが発揮できませんし、変換時の誤差が殊の外大きいです。演算子のオーバーロードをもっと準備してもよいのですが、最低限は記載していると思います。よくよく見てみると足し算や引き算は普通の足し算、引き算を使っているし、乗算や除算は計算時に64bitに拡張しているだけで、いつもの掛け算、割り算と対して変わりありません。固定小数点数というのはつまるところ演算には整数型の計算機を使用して、表現だけ小数点にしたものに他ならないのです。補足ですが整数計算と同じ計算機を使用することから浮動小数点の時のような情報落ち、桁落ちという誤差は発生しません。このクラスを使用するクライアントコードは次のようになります。
#include "CFix16.h"
int _tmain(int argc, _TCHAR* argv[])
{
CFix16 x, y, z, w;
x = 12;
y = 16;
z = ( x * y );
w = ( x / y );
x.tofloat();
z.tofloat();
w.tofloat();
return 0;
}
この様なクラスを使用して、今まで紹介した初等関数の計算を書き換えてみると固定小数点数でのライブラリが作成できるかと思います。