ぐだぐだ低レベルプログラミング(149)ARM64(AArach64)SIMD 逆数系

Joseph Halfmoon

前回は、御勝手命名「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個の命令ニーモニックです。

      1. FRECPE、 Floating-point reciprocal estimate
      2. FRSQRTE、 Floating-point reciprocal square root estimate
      3. URECPE、 Unsigned reciprocal estimate
      4. 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

実行結果が以下に。recpeResults

urecpeの最初の00番、本当は浮動小数点換算4.0となるべきですが、2以上のお答えはみんなオール1が返るみたいデス。それにしても固定小数点の扱い、入力のときと出力のときでズレとる感じで気持ちが悪いデス。こういう解釈でいいのか?

ぐだぐだ低レベルプログラミング(148)ARM64(AArach64)SIMD 反転系 へ戻る

ぐだぐだ低レベルプログラミング(150)ARM64(AArach64)SIMD 拡張系 へ進む