ぐだぐだ低レベルプログラミング(155)ARM64(AArach64)SIMD FRINTx

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

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です。一番常識的な四捨五入な奴(負のときは知らんけど。)FRINTA

    • FRINTMとFRINTN

正負の無限大方向への一方通行なやつ。最初はマイナス方向。
FRINTM
続いてプラス方向。FRINTP

 

    • FRINTN

偶数丸め。そのままの入力だと、Aのときと差が分からないので、ことさら入力値を変えました。2.5を丸めると2.0になってしまいます。常識人に示すと「どゆこと」と怒られそう。FRINTN

 

    • FRINTZ(および今回のFRINTI)

ゼロ方向への丸め。今回はFRINTIもカレントの丸めモードはゼロ方向丸めに設定したので結果は同じです。FRINTZ

 

    • FRINTX(およびFRINTI)

さて、メインイベンターのFRINTXです。FRINTXは変換値としてはFRINTIとまったく同じ結果を与えるので結果だけみても差は分かりません。差は小さく赤く色つけたところです。FRINTX

数値計算のプロの人はこういう丸めを使い分けてるんだろうけどなあ、素人の老人にはメンドイばかりよ。不埒な。

ぐだぐだ低レベルプログラミング(154)ARM64(AArach64)SIMD from整数 へ戻る

ぐだぐだ低レベルプログラミング(155)ARM64(AArach64)SIMD FCVTxy へ進む