
前回は、御勝手命名「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
御勝手分類、SIMD「逆数系」
今回、練習するためにまとめてしまったのは以下の4個の命令ニーモニックです。
-
-
- FRECPE、 Floating-point reciprocal estimate
- FRSQRTE、 Floating-point reciprocal square root estimate
- URECPE、 Unsigned reciprocal estimate
- URSQRTE、 Unsigned reciprocal sqrt estimate estimate
-
皆 reciprocal(逆数)を求める命令であって、estimate(だいたいね、という感じ?)です。所定の形式の有効桁数まで求めるのでなく、ごく大雑把な推定値を求めるもの。精度が必要だったら、後は第134回でやったニュートン・ラフソン法向け命令があるのでこの推定値を出発点にしてニュートン法で求めてね、という感じです。精度についてどんなもんなのだろ~と思ったら、以下、先達の方の記事を発見しました。まあA32のNEONの記事っすけど多分今もOKだろ~と。
NEONの逆数推定命令の精度 : Accuracy of NEON reciprocal estimation instruction
なお、「1」「2」の浮動小数の逆数推定命令にはARMv8.2以降ではHalf precisionも定義されているのですが、今回はARMv8.0なので単精度からです。
また、「3」「4」のUnsigned系は符合無整数といっても「固定小数点」です。これの入出力の扱いがちょいとクセ強な気がしないでもないっす。
実験に使ったアセンブリ言語記述の被テスト関数
いつものように手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下です。浮動小数、固定小数点(符合無整数)ともオペランドは32ビット幅で4要素のときのみ練習してます。
.globl frecpe4V, frsqrte4V, urecpe4V, ursqrte4V
.text
.balign 4
frecpe4V:
ld1 {v1.4S}, [x0], #16
frecpe v0.4S, v1.4S
st1 {v0.4S}, [x0]
ret
frsqrte4V:
ld1 {v1.4S}, [x0], #16
frsqrte v0.4S, v1.4S
st1 {v0.4S}, [x0]
ret
urecpe4V:
ld1 {v1.4S}, [x0], #16
urecpe v0.4S, v1.4S
st1 {v0.4S}, [x0]
ret
ursqrte4V:
ld1 {v1.4S}, [x0], #16
ursqrte v0.4S, v1.4S
st1 {v0.4S}, [x0]
ret
C言語記述のmain関数
上記のアセンブリ言語関数を呼び出すmain関数が以下に。
#include <stdio.h>
#include <stdint.h>
#define MAXMEM (8)
uint32_t TargetMEM[MAXMEM];
float TargetMEM2[MAXMEM];
extern void frecpe4V(float *);
extern void frsqrte4V(float *);
extern void urecpe4V(uint32_t *);
extern void ursqrte4V(uint32_t *);
void initTGT() {
TargetMEM[0] = 0x40000000;
TargetMEM[1] = 0x80000000;
TargetMEM[2] = 0xE0000000;
TargetMEM[3] = 0xFFFFFFFF;
TargetMEM[4] = 0x0;
TargetMEM[5] = 0x0;
TargetMEM[6] = 0x0;
TargetMEM[7] = 0x0;
}
void initTGT2() {
TargetMEM2[0] = 0.25;
TargetMEM2[1] = 0.5;
TargetMEM2[2] = 0.875;
TargetMEM2[3] = 1.0;
TargetMEM2[4] = 0.0;
TargetMEM2[5] = 0.0;
TargetMEM2[6] = 0.0;
TargetMEM2[7] = 0.0;
}
void dumpTGT(const char *arg) {
printf("%s\n", arg);
for (int i=0; i < 4; i++) {
printf("%02d: 0x%08x -(%s)-> 0x%08x(%f)\n", i, TargetMEM[i], arg, TargetMEM[i+4], (float)TargetMEM[i+4]/(float)0x80000000);
}
}
void dumpTGT2(const char *arg) {
printf("%s\n", arg);
for (int i=0; i < 4; i++) {
printf("%02d: %f -(%s)-> %f\n", i, TargetMEM2[i], arg, TargetMEM2[i+4]);
}
}
int main(void) {
initTGT2();
frecpe4V(TargetMEM2);
dumpTGT2("frecpe");
initTGT2();
frsqrte4V(TargetMEM2);
dumpTGT2("frsqrte");
initTGT();
urecpe4V(TargetMEM);
dumpTGT("urecpe");
initTGT();
ursqrte4V(TargetMEM);
dumpTGT("ursqrte");
return 0;
}
実機実行結果の確認
以下のようにしてビルドして実行しています。
$ gcc -g -O0 simdrecpe.c simdrecpe.s $ ./a.out
urecpeの最初の00番、本当は浮動小数点換算4.0となるべきですが、2以上のお答えはみんなオール1が返るみたいデス。それにしても固定小数点の扱い、入力のときと出力のときでズレとる感じで気持ちが悪いデス。こういう解釈でいいのか?
