前回につづき今回も勝手命名「SIMD整数変換系」の命令の練習です。浮動小数値を浮動小数形式のまま整数に丸めるもの。メンドイので後回しにしたかった奴ら。浮動小数に「つきもの」の丸めモロだしです。丸めの差が見えるように入力値を選ばねばならないけれど、一部の結果はステータスフラグまで見に行かないとわかりませぬ。メンドイ。
※「ぐだぐだ低レベルプログラミング」投稿順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
FRINTxとFCVTxy
SIMDレジスタに詰まっている浮動小数要素を「整数」に変換する命令としては以下の2系統あります。
-
- FRINTx
- FCVTxy
FRINTxは、浮動小数要素を浮動小数形式ではあるものの整数ピタリの値に丸めるもの。一方、FCVTxyは浮動小数要素を丸めて整数形式に変換してしまうものです。どちらも x のところに丸めモードを指定する文字が入ります。FCVTxyのyには変換先の整数が符号無(u)か符号有(s)かを指定です。
今回は、浮動小数を浮動小数に変換するFRINTxを練習してみます。
FRINTxの丸めモード
FRINTxの x のところに入る丸めモードには以下があります。
-
- M、toward minus infinity
- P、toward positive infinity
- A、nearest with ties to away
- N、nearest with ties to even
- Z、toward zero
- I、current rounding mode
- X、exact, using current rounding mode
分かり易いものから説明すると、Mはマイナス無限大方向への丸め、Pはプラス無限大方向への丸めです。「一方通行」なので分かり易い?
続くAとNは浮動小数処理ではお馴染みですが、慣れないとクセ強な奴らです。所謂四捨五入に近い挙動ではあります。どちらも一番近い整数に丸めることになっているけれど「真ん中」のときの挙動が異なります。Aであると「(原点から)遠い方」へ飛ばされます。Nは浮動小数処理で一番使われる偶数丸めというやつ。真ん中の場合は偶数の値の方に丸められます。
Zは、所謂「切り捨て」に近いです。小数点以下を切り捨ててゼロ方向に丸めます。
ううむ、小学校のときに「四捨五入」や「切り捨て」習ったけれども、そのときは負の数が出てこなかった遠い記憶。負の数の取り扱いまで入れておいてくれないと丸めの問題はメンドさも半分だわな。
IとXは、いずれもFPCRの丸めモードビットの設定に応じて動作が変わる命令です。なお丸めモードビットは4ビットしかないので、上記の中の
-
- M
- P
- N
- Z
のみ実装されています。Aは「常識的な四捨五入」なんだけれども「デフォルト」の丸めモードとしては採用してもらえてないです。Nを使えと。
適用される丸めモード自体は、IとXで同じなのですが、命令が2種あるのは、
-
- Exact
- InExact
の識別のためです。元がピタリ整数として表現される値であればExactだけれど、そうでないときはInExactだと。Iであればその辺「コマケーところは踏みつぶして」通り過ぎるのですが、Xの場合、InExact例外を発生させることが可能です。例外を発生させない場合でもFPSRの中にInExactを示すフラグが上がっているので分かります。オフサイド・フラグみたいなもん?
実験につかったアセンブリ言語記述の被テスト関数
例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下に。ターゲット機は例によって半精度浮動小数のサポートがなく、単精度か倍精度なので単精度で練習してます。どれも入力値を与えて、変換して、結果を取り出すというスタイルですが、FRINTXのみは浮動小数のステータスレジスタ(FPSR)を読まねばその効用が分からないのでちょっとメンドいコードになってます。
.globl readfpcr, readfpsr, writefpcr, frinta4V, frintm4V, frintn4V, frintp4V, frintz4V, frintx4V, frinti4V .text .balign 4 readfpcr: mrs x0, fpcr ret readfpsr: mrs x0, fpsr ret writefpcr: msr fpcr, x0 ret frinta4V: ld1 {v0.4S, v1.4S}, [x0] frinta v0.4S, v1.4S st1 {v0.4S}, [x0] ret frintm4V: ld1 {v0.4S, v1.4S}, [x0] frintm v0.4S, v1.4S st1 {v0.4S}, [x0] ret frintn4V: ld1 {v0.4S, v1.4S}, [x0] frintn v0.4S, v1.4S st1 {v0.4S}, [x0] ret frintp4V: ld1 {v0.4S, v1.4S}, [x0] frintp v0.4S, v1.4S st1 {v0.4S}, [x0] ret frintz4V: ld1 {v0.4S, v1.4S}, [x0] frintz v0.4S, v1.4S st1 {v0.4S}, [x0] ret frinti4V: ld1 {v0.4S, v1.4S}, [x0] frinti v0.4S, v1.4S st1 {v0.4S}, [x0] ret frintx4V: ld1 {v0.4S, v1.4S}, [x0] mrs x2, fpsr and x2, x2, x1 msr fpsr, x2 frintx v0.4S, v1.4S st1 {v0.4S}, [x0] mrs x0, fpsr ret
C言語記述のmain関数
上記のアセンブリ言語関数を呼び出すmain関数が以下に。命令により結果が分かり易いように入力値をちょいと出し入れしたので、初期値は3種類も使ってしまいました。いかにも丸めはメンドイのよ。
#include <stdio.h> #include <stdint.h> #include <float.h> #define MAXMEM (8) float TargetMEM[MAXMEM]; extern uint32_t readfpcr(void); extern uint32_t readfpsr(void); extern void writefpcr(uint32_t); extern void frinta4V(float *); extern void frintm4V(float *); extern void frintn4V(float *); extern void frintp4V(float *); extern void frintz4V(float *); extern void frinti4V(float *); extern uint32_t frintx4V(float *, uint32_t msk); void initTGT() { TargetMEM[0] = 0.0f; TargetMEM[1] = 0.0f; TargetMEM[2] = 0.0f; TargetMEM[3] = 0.0f; TargetMEM[4] = 1.5f; TargetMEM[5] = 1.51f; TargetMEM[6] = -1.5f; TargetMEM[7] = -1.51f; } void initTGT2() { TargetMEM[0] = 0.0f; TargetMEM[1] = 0.0f; TargetMEM[2] = 0.0f; TargetMEM[3] = 0.0f; TargetMEM[4] = 2.5f; TargetMEM[5] = 2.51f; TargetMEM[6] = -2.5f; TargetMEM[7] = -2.51f; } void initTGT3() { TargetMEM[0] = 0.0f; TargetMEM[1] = 0.0f; TargetMEM[2] = 0.0f; TargetMEM[3] = 0.0f; TargetMEM[4] = 1.0f; TargetMEM[5] = 1.0f; TargetMEM[6] = -1.0f; TargetMEM[7] = -1.0f; } void dumpTGT(const char *arg) { printf("%s\n", arg); for (int i=0; i<4; i++) { printf("%02d: %f -(%s)-> %f\n", i, TargetMEM[i+4], arg, TargetMEM[i]); } } int main(void) { uint32_t temp = readfpcr(); writefpcr(temp | 0xC00000); //toward zero initTGT(); frinta4V(TargetMEM); dumpTGT("frinta4V"); initTGT(); frintm4V(TargetMEM); dumpTGT("frintm4V"); initTGT2(); frintn4V(TargetMEM); dumpTGT("frintn4V"); initTGT(); frintp4V(TargetMEM); dumpTGT("frintp4V"); initTGT(); frintz4V(TargetMEM); dumpTGT("frintz4V"); initTGT(); frinti4V(TargetMEM); dumpTGT("frinti4V"); initTGT(); temp = frintx4V(TargetMEM, 0xFFFFFFEF); dumpTGT("frintx4V"); printf("FPSR = 0x%08x\n", temp); initTGT3(); temp = frintx4V(TargetMEM, 0xFFFFFFEF); dumpTGT("frintx4V"); printf("FPSR = 0x%08x\n", temp); return 0; }
実機実行結果の確認
以下のようにしてビルドして実行しています。
$ gcc -g -O0 simdFINT.c simdFINT.s $ ./a.out
-
- FRINTA
最初はFRINTAです。一番常識的な四捨五入な奴(負のときは知らんけど。)
-
- FRINTMとFRINTN
正負の無限大方向への一方通行なやつ。最初はマイナス方向。
続いてプラス方向。
-
- FRINTN
偶数丸め。そのままの入力だと、Aのときと差が分からないので、ことさら入力値を変えました。2.5を丸めると2.0になってしまいます。常識人に示すと「どゆこと」と怒られそう。
-
- FRINTZ(および今回のFRINTI)
ゼロ方向への丸め。今回はFRINTIもカレントの丸めモードはゼロ方向丸めに設定したので結果は同じです。
-
- FRINTX(およびFRINTI)
さて、メインイベンターのFRINTXです。FRINTXは変換値としてはFRINTIとまったく同じ結果を与えるので結果だけみても差は分かりません。差は小さく赤く色つけたところです。
数値計算のプロの人はこういう丸めを使い分けてるんだろうけどなあ、素人の老人にはメンドイばかりよ。不埒な。