ちょっと Tea Time!? 浮動小数点フォーマットの憂鬱 2025.9.23

MZ80化計画をすすめています。CPM80のモニター画面回りの一体化です。
さすがにZ80で800x480のWVGAにドットフォントを打たせるには、非力すぎます。
そのため、RP2040マイコンをアクセラレータ代わりに使っています。
しかし、せっかくRP2040をつかうのなら、画面回りだけではなく、
算術演算の支援もできないかと考えています。
 Z80なんかで三角関数などを計算させると、とてつもない時間がかかりますが、
流石にRP2040だとハードウエア乗算機能はもとより、いわゆるコープロみたいなものも、
実装されている(と期待したい)でしょうから、かなりの高速化が見込めます。

まずは、外付けの呼び出し関数みたいな形で使えるように検討しましょう!

どころで、浮動小数点の形サイズはどうなっている?

MZ80化のベースとなっているCPM80には3つのCコンパイラを組み込んでいます。
で、まずはそこで使われる変数のサイズを調べてみました。
比較のために標準的なRasPiについて記しています。というのもRasPiとPICO(RP2040)
では変数サイズが同じなので、こちらで代用です。

RasPi
(PICOと同じ)
Hi-tech C
(CPM80)
AZTEC-C
(CPM80)
BDS-C
(CPM80)
double 8 4 8 N.A
float 4 4 4 N.A
int 4 2 2 2
char 1 1 1 1

調べた結果が上表になりました。AZTEC-Cがdouble形だと8バイトあり、
RasPiと同じです。Hi-tech Cについてはdouble型の宣言はできますが、
中身はFloatと同じ4バイトです。ちなみに、一番よくつかうBDS-Cでは
浮動小数点は使えません。これが、一番コンパイルリンクが速いので、
つかいやすのになあ〜。

AZTEC-Cは難あり?

上記の結果から、AZTEC-C用にアクセラレータを組み込もうと思いましたが、
浮動小数点のフォーマットを調べようとして、次のような共用体(union)をつかったら、
プログラムは動作するのですが、なぜか特定のファイルを壊してしまいます。

union {
   float a;
   char b[4];
} c;

具体的にはテキストエディタであるWordmasterfが動かなくなります。
その都度、システムを立ち上げて、新しいファイルをコピーし直せば動くのですが、
いちいちそんなことはしてられません。

ということで、float精度しかサポートしていませんが、Hi-tech C用に検討することとしました。

浮動小数点フォーマットがバラバラだあ〜

この検討を進めるにあたり、一番気になったのが浮動小数点のフォーマットです。
本来はIEEE754に準拠していてくれればいいのですが、この規格自体が1985年制定(みたい)
なので、ちょうど各種コンパイラのリリースされた時期と被ります。
 となると、各社で独自のフォーマットにしている可能性があります。

そこで、float(単精度)でのフォーマットを調べてみました。
実数で-4〜4までのバイト毎の値をを列挙です。
もう、見事にバラバラです。AZTEC−Cの場合は、並びも反対です。
Hi TECH CもRasPiと異なりますが、並び自体はRasPiと同じく下位バイトから先に来ています。

Raspberry Pi Hi-tech C (CPM80) AZTEC-C (CPM80)
数値(10進)
Float
c.b[3] c.b[2] c.b[1] c.b[0] c.b[3] c.b[2] c.b[1] c.b[0] c.b[0] c.b[1] c.b[2] c.b[3]
-4.0 C0 80 00 00 C3 80 00 00 C1 04 00 00
-3.0 C0 40 00 00 C2 C0 00 00 C1 03 00 00
-2.0 C0 00 00 00 C2 80 00 00 C1 02 00 00
-1.0 BF 00 00 00 C1 80 00 00 C1 01 00 00
0.0 00 00 00 00 00 00 00 00 00 00 00 00
1.0 3F 80 00 00 41 80 00 00 41 01 00 00
2.0 40 00 00 00 42 80 00 00 41 02 00 00
3.0 40 40 00 00 42 C0 00 00 41 03 00 00
4.0 40 80 00 00 43 80 00 00 41 04 00 00

じっくり眺めてフォーマットを読み取りましょう!

単精度なので32bitありますが、最初の12bitを抜き出せば、
どのような構造(フォーマット)になっているかはわかるでしょう。

1.RasPiの場合

これは調べるまでもなくIEE754に準拠しています。
それの確認のためにも、ビットで整理してみました。

Raspberry Pi
数値(10進)
Float
S
G
N
指数
(8Bit)
仮数
(23Bitの上位4ビット)
-4.0 1 1 0 0 0 0 0 0 1 0 0 0
-3.0 1 1 0 0 0 0 0 0 0 1 0 0
-2.0 1 1 0 0 0 0 0 0 0 0 0 0
-1.0 1 0 1 1 1 1 1 1 1 0 0 0
0.0 0 0 0 0 0 0 0 0 0 0 0 0
1.0 0 0 1 1 1 1 1 1 1 0 0 0
2.0 0 1 0 0 0 0 0 0 0 0 0 0
3.0 0 1 0 0 0 0 0 0 0 1 0 0
4.0 0 1 0 0 0 0 0 0 1 0 0 0

数値の表現は下記式で表されます。

SGN x  2^(指数 - 127) x 1.XXXXX (xxxx:仮数)


IEE754については、色々なHPで解説されていますので、
そちらを参照ください。

2.Hi Tech Cの場合はどうだ?

じっくりと表を眺めます。正負(sign)ビットは先頭にあるようです。
IEE754と異なるのは、指数部が7bitしかありません。ちなみに
IEE754は8ビットです。そのあとは、仮数部になるようです。

Hi-TECH C(CPM80)
数値(10進)
Float
S
G
N
指数
(7Bit)
仮数
(24Bitの上位4ビット)
-4.0 1 1 0 0 0 0 1 1 0 0 0
-3.0 1 1 0 0 0 0 1 0 1 1 0 0
-2.0 1 1 0 0 0 0 1 0 0 0 0
-1.0 1 1 0 0 0 0 0 1 0 0 0
0.0 0 0 0 0 0 0 0 0 0 0 0 0
1.0 0 1 0 0 0 0 0 1 0 0 0
2.0 0 1 0 0 0 0 1 0 0 0 0
3.0 0 1 0 0 0 0 1 0 1 0 0
4.0 0 1 0 0 0 0 1 1 0 0 0

単に、指数部のビット数の違いだけかと思いきや、仮数部もIEEE754と異っていそうです。

Hi TECH Cだと、下記のように思われます。
例えば上表で3.0の場合をみると、仮数部は1100となっており、
これは
0.5x1+0.25X1+0.125x0+0.0625x0 = 0.75
を示しているようです。
指数部は1000010で10進で66ですから、オフセット値を
64とすると
 2^(66-64)=4
になりますから、仮数部と乗算して
 2^(66-64) x 0.5x1+0.25X1+0.125x0+0.0625x0 = 4 x 0.75 = 3.0
となります。この考え方で、他の数値も表されているようです。

どうやら、下記式で表されるようです。

SGN x  2^(指数 - 64) x 0.XXXXX (xxxx:仮数)


とにかく変換が必要


Hi-tech CのfloatのデータをそのままRP2040マイコンに渡しても、まともに計算できません。
値が同じになるようにIEEE754フォーマットに変換する必要があります。
この変換は、勿論のことRP2040結果がでても、Hi tech Cに引き渡す前にも、
再度行う必要があります。
 これらの変換なんか加えていたら、あまり速度も速くならないじゃないかなあ〜
という気が沸いてこないでもないですが、そこは確認も含めて、変換方法を考えてみましょう。

変換で肝になるのが、仮数部を合わせることです。
 IEEE754     1.XXXX (XXXX 仮数部)
 Hi-TECH C   0.XXXX (XXXX 仮数部)

となっており、IEEE754(RasPi)では1以上の小数点以下が仮数部になっています。
片や、HI TECH Cでは1未満の小数点以下が仮数部です。

そこで、仮数部を強制的なビットシフトで誤魔化します

1)IEEE754からHI TECH C へ
 仮数部を右方向へ1ビットシフトです。そうすれば、強制的に
 0.XXXXの形になります。右方向へのビットシフトは2で割るのと
 同じですから、指数部を+1して補正します。
 2^m x 1.abcde = 2^(m+1) x 0.1abcde
 これでIEEE754のフォーマットをHi TECH Cへ変換(戻す)ことができるはずです。

2)Hi TECH C から IEEE754へ
  (結構ややこしそう。寝ながら検討予定(笑))
ややしそうと思ったのは、IEEE754では仮数部が1以上であることが前提
となっていますが、HITECH-Cで1未満で0.XXXの形式になっています。
もし、0.0XXXとか0.00XXXとかのようになっていたら、X.XXXXの形になるまで
ビットシフトを繰り返さないといけません。しかし、よく考えたら
指数形式をとっている以上、必ず0.XXXXとなっていて、1つのビットシフトで、
かならず1以上になるはずです。そう考えれば、1)の場合の反対を行うだけです。
すなわち
 仮数部を左方向へ1ビットシフトです。そうすれば、強制的に
 X.XXXXの形になります。左方向へのビットシフトは2を掛けるのと
 同じですから、指数部を-1して補正します。
 2^m x 0.abcde = 2^(m-1) x a.bcde (a is always 1)
 これでHi TECH CをIEEE754のフォーマットへ変換です。

#と考えると、HITECH-Cは1ビット分精度を無駄にしていることになります。
すなわち、仮数部が0.XXXXXになっていますが、最初のXすなわち0.XXXXX
Xは必ず1であるから、表記を無視しても構わないはずです。まあ、それを
やられるとフォーマットの解析が無茶苦茶難解ですが。それに対して、IEEE754
は、やはりよく考えているなあ〜。仮数部を.XXXXとすることで最初の
省略しています。

気を付けないといけないのは、ゼロ値の取り扱いです。
指数と仮数がゼロになっている場合は、下手に扱うとエラーになりそうなので、
そのあたりは変換する関数の中で調整です。
 例えば SQRT(0)=0 などかなあ〜、

実装してみましょう! 2025.9.24

無駄と思われるビットシフトがありますが、まずは正確に動くことを念頭にして、
IEEE754とHITECH-C間での変換ルーチンを作成です。

動けば、コードを見直していきましょう。まあ、ビットシフト演算の無駄なんか
全体からすれば、無視できるでしょうけどね。

// IEEE754をHITECH-Cへ変換
uint32_t IEEEtoHITECH(uint32_t n)
{
 uint32_t sgn,ind,fract;

 sgn = n & 0x80000000;
 ind = (n & 0x7f800000) >> 23;
 ind -=62;
 ind = ind << 24;
 fract= (n | 0x00800000) & 0x00ffffff;
 n = fract | ind | sgn;
 return(n);
}

// HITECH-CをIEEE754へ変換
uint32_t HITECHtoIEEE(uint32_t n)
{
 uint32_t sgn,ind,fract;

 sgn = n & 0x80000000;
 ind = (n & 0x7f000000) >> 24;
 ind += (63 - 1);
 ind = ind << 23;
 fract= n & 0x007fffff;
 n = fract | ind | sgn;
 return(n);
}

爆速! 

まずは、正確に動作、すなわち演算結果が正しいことを確認した後に
ベンチマークです。

平方根(sqrt)計算をさせてみました。
ちなみにHI-TECH Cの浮動小数点使用時のコンパイルリンクは
A>C FILE.C -LF 
で、オプションに-LFを追加します(備忘録)。

さて、計測結果は下記のようになりました。

HI-TECH Cライブラリ RP2040ライブラリ
SQRT(a)

a=2.0の場合
29.4秒 (20,000回)

1.47ms/回
sqrt(a)
9.9秒 (200,000回)

0.0495ms/回
gsqrt(a)

1.47msから0.0495msへ短縮ですから、約30倍ほど速くなりました。
爆速です。
引数引き渡しや、サブルーチンの呼び出し、そして変数変換など、
かなりのオーバヘッドがありますが、流石にRP2040の速さが勝っているので
かなりの高速化がはかれました。単純にRP2040だけだったら1000倍以上は
速くなっているだろうなあ〜。

四則演算はどうだろう?

関数演算はかなり速くなりそうですが、やっぱり演算のボリュームゾーンは
四則演算です。そこで、乗算と除算で調べてみました。

結果としては、乗算はほぼ同じ、除算は2倍ほど速くなったようで.,
まああまり差はないかな〜というところです.,
流石に四則演算になると、引き渡すパラメータが2個になって、
その変換などを含めるとオーバヘッドが大きすぎるというところでしょうか.,
それに、関数呼び出しになるので、記述がすこし面倒です.,

HI-TECH Cライブラリ RP2040ライブラリ
乗算

a=2.0;
b=3.0;
11.5秒 (200,000回)

57.5us/回

c=a*b
11.8秒 (200,000回)

59us/回
c=gmu(a,b)
除算

a=2.0;
b=30.0;
27.2秒 (200,000回)

136us/回

c=a/b
12.0秒 (200,000回)

60us
c=gdiv(a,b)

まとめ
 HiTECH-Cコンパイラの算術演算にRP2040を活用してみました.,
平方根では約30倍高速になりました。関数演算にはかなり効果が見込めそうです.。
片や四則演算ではほぼ同じです(除算は倍程度速くなる)。
速くならない原因はオーバヘッドが大きいためですが、関数呼び出しとかになり、
式の記述も複雑になることから、四則演算での効果は限定的でしょう。

(おしまい)