前回のFCVT命令群は浮動小数点数を整数に変換するものでした。今回のFCVTは浮動小数点数を固定小数点数に変換する命令です。ハード上は整数、でも小数点位置を心の中に秘めて?いるもの。デジタル信号処理では必須。プログラマが小数点位置を管理しないとならないのでメンドイですが高速に計算できます。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
※実機動作確認には以下を使用しております。
-
- Raspberry Pi 4 model B、Cortex-A72コア(ARMv8-A)
- Raspberry Pi OS (64bit) bullseye
- gcc (Debian 10.2.1-6) 10.2.1 20210110
ARMv8もいろいろレベルがあり、Arm Cortex-A72はARMv8の中でもベーシックな(命令数の少ない)ARMv8p0です。
※A64の最新のマニュアルは以下でダウンロード可能です。
Arm Architecture Reference Manual for A-profile architecture
固定小数点数を扱えるFCVT命令
FCVT命令群のうち、一部の命令は固定小数点数を扱うことができます。ニーモニック的にいうと
-
- FCVTZS
- FCVTZU
- FJCVTZS
の3つが固定小数点数用ということになっているみたいです。ただし、3番目のFJCVTZS命令(FJCVTZSのJはJavaScriptからきてるみたいです)は、ARMv8でもv8.3以降の拡張であるようなので、今回ターゲット機のARMv8.0では使えないようです(よかった、これ以上ケースが増えなくて。)
よって上2つが実験可能ではあります。前回のFCVT命令群と異なり、固定小数点数を扱える命令はZ(ゼロ方向丸め)のみです。組み合わせは大幅減であるのですが、例によって、変換先の整数レジスタはW(32ビット幅)とX(64ビット幅)をとることができ、変換もとはS(単精度)とD(倍精度)をとることができます(ARMv8.2以降では半精度も使えます。)そして肝心の固定小数点位置の指定があります。組み合わせが多くなりすぎるので、符号無の FCVTZU命令 だけを練習してみることにしました。手抜きだな。
固定小数点数
今回実験では、同じ32ビット幅の整数レジスタに収まる固定小数点数ですが異なるフォーマット(異なる小数点位置)の2種類を扱ってみたいと思います。2進表現したときに下何ビットを小数点以下(分数の分子部分)として扱うかを、Qチョメチョメ(チョメチョメにはビット数が入る)と表現することが多いです。全体のデータ幅が32ビットであるので、ここでは便宜的に 32Q3と32Q12などと表現してます。
固定小数点数は、整数演算命令で「計算」可能ですが、掛け算すると固定小数点のビット数が倍増してしまいます。例えばQ12であればQ24という具合です。計算後Q12に戻すのであればシフトしないとならないですし、Q24のまま計算を続けるのであれば計算相手の数はQ24でないとなりませぬ。一方、足し算、引き算ではQのビット数は変わらないです。
表現できるダイナミックレンジが限られるので、ちょっと気を抜くと必要な部分が消えてしまったりします。固定小数点での計算では各計算ステップ毎にデータのレンジが想定しているフォーマットに収まっていることを逐一確認して「設計」する必要があります。
今回実験のアセンブリ言語関数
今回も例によって関数プロローグもエピローグもない、手抜きな被テストアセンブラ関数群が以下に。
.globl fcvtzuWS3, fcvtzuXS3, fcvtzuWS12, fcvtzuXS12, readfpcr, writefpcr .text .balign 4 fcvtzuWS3: fcvtzu w0, s0, #3 ret fcvtzuXS3: fcvtzu x0, s0, #3 ret fcvtzuWS12: fcvtzu w0, s0, #12 ret fcvtzuXS12: fcvtzu x0, s0, #12 ret readfpcr: mrs x0, fpcr ret writefpcr: msr fpcr, x0 ret
C言語記述のmain関数
これまた手抜きなC言語記述のテスト駆動部が以下に。固定小数点数のいい加減な表示ありとしました。浮動小数点数は10進で書きたいし、固定小数点数の全体は16進でビットをみたい、でも小数点部分は10進小数にしたいということで、テキトーに雰囲気を見るだけ。
#include <stdio.h> #include <stdint.h> extern uint32_t fcvtzuWS3(uint32_t, float); extern uint64_t fcvtzuXS3(uint64_t, float); extern uint32_t fcvtzuWS12(uint32_t, float); extern uint64_t fcvtzuXS12(uint64_t, float); extern uint64_t readfpcr(uint64_t); extern uint64_t writefpcr(uint64_t); void printFixedPoint(uint32_t arg, uint32_t fxp) { uint32_t ipart = arg >> fxp; uint32_t msk = 0; for (int i=0; i < fxp; i++) { msk = (msk << 1) | 1; } float fpart = (float)(arg & msk) / (msk + 1); printf("%d %f\n", ipart, fpart); } int main(void) { union { double resultD; uint64_t result64; } u64; union { float resultS; uint32_t result32; } u32; u32.result32 = fcvtzuWS3(0, 7777.5555f); printf ("fcvtzuWS3(0 7777.5555f): 0x%08x\n", u32.result32); printFixedPoint(u32.result32, 3); u64.result64 = fcvtzuXS3(0, 7777.5555f); printf ("fcvtzuXS3(0 7777.5555f): 0x%016lx\n", u64.result64); printFixedPoint((uint32_t)u64.result64, 3); u32.result32 = fcvtzuWS12(0, 7777.5555f); printf ("fcvtzuWS12(0 7777.5555f): 0x%08x\n", u32.result32); printFixedPoint(u32.result32, 12); u64.result64 = fcvtzuXS12(0, 7777.5555f); printf ("fcvtzuXS12(0 7777.5555f): 0x%016lx\n", u64.result64); printFixedPoint((uint32_t)u64.result64, 12); return 0; }
ビルドして実行
元は同じ浮動小数点数なのに、変換後のビットパターンは、固定小数点位置の指定により異なることにご注目あれ。その数を整数部分と小数部分に分けて表記してみると。当然だけれど整数部分はどのフォーマットでも完全一致。一方、小数点位置がビット3だと、0.5555は切り捨てられて0.5になるけれど、小数点位置がビット12だと、0.5555の2進パターンをゼロ方向に丸めても0.555664に見える数になるのね?ホントか?