ちょっと 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 | 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 | 1 | 0 | 0 | 0 |
-1.0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 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 | 1 | 0 | 0 | 0 |
2.0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
3.0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
4.0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 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
は、やはりよく考えているなあ〜。仮数部を1.XXXXとすることで最初の1は
省略しています。
気を付けないといけないのは、ゼロ値の取り扱いです。
指数と仮数がゼロになっている場合は、下手に扱うとエラーになりそうなので、
そのあたりは変換する関数の中で調整です。
例えば 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倍高速になりました。関数演算にはかなり効果が見込めそうです.。
片や四則演算ではほぼ同じです(除算は倍程度速くなる)。
速くならない原因はオーバヘッドが大きいためですが、関数呼び出しとかになり、
式の記述も複雑になることから、四則演算での効果は限定的でしょう。
(おしまい)