前回は、御勝手命名「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が返るみたいデス。それにしても固定小数点の扱い、入力のときと出力のときでズレとる感じで気持ちが悪いデス。こういう解釈でいいのか?