前回から「Across Vector」、SIMDレジスタに並ぶ各要素を横断的に処理(縮約)して1個のスカラー値を得る命令を練習してます。今回はFMAXVとその一族です。浮動小数要素のMAXおよびMINを求める命令ですが2命令づつあります。同じMAXとる命令でもNaNの扱いで2種類に分かれるっと。メンドクセー奴だよ。
※「ぐだぐだ低レベルプログラミング」投稿順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
FMAXVとFMAXNMV、FMINVとFMINNMV
V付きでない、スカラー命令については以下の過去回でやってます。
ぐだぐだ低レベルプログラミング(115)ARM64(AArach64)FMAX, FMIN
素のFMAXVやFMINVが、ソースのベクトル要素の中に1個でもNaN(ノット・ア・ナンバー)を見つけるとNaNを返してしまうのに対して、FMAXNMVとFMINNMVは、NaNなど見えなかったテイにて、数値的にMAXまたはMINのものを返してくれるという違いです。命令間の差異が見えるのはQuiet NaNが紛れ込んでいたときだけっす。
実験につかったアセンブリ言語記述の被テスト関数
例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下に。ARMv8.0は「幸い」半精度浮動小数に対応していないので、いつもの手抜きでソース要素が単精度浮動小数(32bit)のときのみ練習してます。
.globl fmaxv4V, fmaxnmv4V, fminv4V, fminnmv4V, readfpsr .text .balign 4 fmaxv4V: ld1 {v0.4S, v1.4S}, [x0] fmaxv s0, v1.4S st1 {v0.4S}, [x0] ret fmaxnmv4V: ld1 {v0.4S, v1.4S}, [x0] fmaxnmv s0, v1.4S st1 {v0.4S}, [x0] ret fminv4V: ld1 {v0.4S, v1.4S}, [x0] fminv s0, v1.4S st1 {v0.4S}, [x0] ret fminnmv4V: ld1 {v0.4S, v1.4S}, [x0] fminnmv s0, v1.4S st1 {v0.4S}, [x0] ret readfpsr: mrs x0, fpsr ret
C言語記述のmain関数
上記のアセンブリ言語関数を呼び出すmain関数が以下に。今回は、単精度浮動小数型(float)のみなのでややスッキリ。ただしNaNが混ざっているときと混ざってないときで挙動の違うところをやらないとならないので、入力ベクトルは最低線の2種類を練習しとります。こんな感じ。
#include <stdio.h> #include <stdint.h> #include <math.h> #define MAXMEM (8) float TargetMEM[MAXMEM]; extern void fmaxv4V(float *); extern void fmaxnmv4V(float *); extern void fminv4V(float *); extern void fminnmv4V(float *); extern uint32_t readfpr(); void initTGT() { TargetMEM[0] = 7.7f; TargetMEM[1] = 7.7f; TargetMEM[2] = 7.7f; TargetMEM[3] = 7.7f; TargetMEM[4] = 1.23f; TargetMEM[5] = 4.56f; TargetMEM[6] = -1.1f; TargetMEM[7] = -5.5f; } void initTGT2() { TargetMEM[0] = 7.7f; TargetMEM[1] = 7.7f; TargetMEM[2] = 7.7f; TargetMEM[3] = 7.7f; TargetMEM[4] = 1.23f; TargetMEM[5] = 4.56f; TargetMEM[6] = NAN; TargetMEM[7] = -5.5f; } void dumpTGT(const char *arg) { printf("%s\n", arg); for (int i=0; i<1; i++) { printf("%02d: %5.2f(%08x)\n", i, TargetMEM[i], TargetMEM[i]); } } int main(void) { initTGT(); fmaxv4V(TargetMEM); dumpTGT("fmaxv normal"); initTGT2(); fmaxv4V(TargetMEM); dumpTGT("fmaxv NAN"); initTGT(); fmaxnmv4V(TargetMEM); dumpTGT("fmaxnmv normal"); initTGT2(); fmaxnmv4V(TargetMEM); dumpTGT("fmaxnmv NAN"); initTGT(); fminv4V(TargetMEM); dumpTGT("fminv normal"); initTGT2(); fminv4V(TargetMEM); dumpTGT("fminv NAN"); initTGT(); fminnmv4V(TargetMEM); dumpTGT("fminnmv normal"); initTGT2(); fminnmv4V(TargetMEM); dumpTGT("fminnmv NAN"); return 0; }
なお、マクロ NAN を使用する場合は、ヘッダ math.h のインクルードが必要であります。
実機実行結果の確認
以下のようにして ビルドして実行しています。
$ gcc -g -O0 simdMAXV.c simdMAXV.s $ ./a.out
標準出力に現れた結果が以下に。
赤枠内がFMAXV、FMINVにNaNを含むベクタを食わせた場合です。有無をいわさずNaNが結果として返ってます。一方、青枠がFMAXNMV、FMINNMVです。NaNなど無かったことにして、残りの数値の中でMAXなりMINなりを見つけて返してきてます。
NaNはメンドイけれども、無いと困るのでショウガナイ。