ぐだぐだ低レベルプログラミング(116)ARM64(AArach64)FCMP

Joseph Halfmoon

今回はFCMP、浮動小数点数の比較命令です。前回のFMAX同様NaN(Not a Number)が絡んできます、メンドクセー。しかしそれ以前にフェイント一発かまされてます。比較結果は条件フラグに反映されるのですが、FPSR(浮動小数ステータス)に条件フラグが存在するのに、PSTATEの条件フラグに反映です。おっと。

※「ぐだぐだ低レベルプログラミング」投稿順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

A64のFCMP命令

FCMP命令は、オペランドの値を比較して、条件フラグを上げ下げするのがお仕事です。

A64(AArach64)には、FPSR(浮動小数点ステータスレジスタ)という64ビット幅の立派なステータスレジスタが存在します。そしてそこにはNZCVの4個の条件フラグも入ってます。しかし、

    • FPSR内のNZCVを使うのはAArach32のとき
    • AArach64のときは整数演算命令と同じPSTATE内のNZCVフラグに反映

ということになっています。多分、32ビットのときの「やっちまった」感の強いFlag配置を深く反省?し(まあ32ビットの頃はFPUはオマケ感が強かったので別枠にしていたのかも)使いやすい形に改めたということなんでしょう。

よってFCMP命令のときは、整数演算命令同様、普通にPSTATE側のNZCVフラグが上げ下げできるので条件判断も簡単です。とは言え、比較結果でどういうフラグが立つの?

頭の中に条件フラグが実装されていない よゐこ のために表を掲げました。

Condition N Z C V
GT 0 0 1 0
LT 1 0 0 0
EQ 0 1 1 0
Unordered 0 0 1 1

大なり(GT)、小なり(LT)、等しい(EQ)以外にUnorderedって何?ここで登場してくるのが NaN一族です。NaN一族は数値と比較することはできないので、比較に使われるとUnorderedとなるみたいっす。

さてメンドクセー奴ら、NaN一族が登場したところで FCMP命令を眺めてみます。

    1. FCMP
    2. FCMPE
    3. FCCMP
    4. FCCMPE

例によってRISCとは思えない命令「豊富な」A64です。4個もあります。それだけではありません。オペランドを見れば、半精度、単精度、倍精度のレジスタをとることができるだけでなく、FCMPとFCMPEについては2オペランド形式に加えて、1オペランド対0.0との比較という形式もあります。

FCMPとFCCMPの違いは、「条件フラグを見て」条件フラグを更新するFCCMPと、「無条件に」更新するFCMPということになります。条件フラグを更新するのにまず条件フラグを確認するなど恐れ入ります。複合的な処理ができるわけね。まあ昔の32ビットArmでは条件フラグの確認をほぼ全命令でやってたわけですが。そしてFCMPの場合「条件合わなかったら」デフォルト値で置き換えると。覚えきれなくなってきました。

そして末尾がEで終わるものと終わらないものの違いは、NaNの取り扱いにあります。どちらもNaNが絡んだ比較はUnorderedになるのですが、FCMP、FCCMPは「静かに(quiet)」通過します。それに対してFCMPE、FCCMPEは「うるさく(signal)」警報を鳴らします。ここで邪険にされていた64ビットのFPSR(浮動小数点ステータスレジスタ)が登場します。FPSRの最下位ビットIOCが立つのです。浮動小数点例外発生が許可されていれば、例外発生!

やっぱりFCMPもメンドクセー奴。

今回実験のアセンブリ言語関数

「いつものように手抜きな」関数プロローグもエピローグもない被テスト関数です。今回は命令実行PSTATEの中のNZCVを確認しないと命令の実行結果が読めないので、戻り値は整数のNZCVフラグとしてあります。また、FCCMPの条件を事前設定するためにNZCVに書き込むことも必要。FCMPEなどではIOC例外フラグも見る必要があるのでFPSRの読み出しも含めてます。

.globl	fcmpS, fcmp0S, fcmpeS, fccmpS, fccmpeS, readfpsr, readNZCV, writeNZCV
.text
.balign	4

fcmpS:
    fcmp s0, s1
    mrs x0, NZCV
    ret

fcmp0S:
    fcmp s0, #0.0
    mrs x0, NZCV
    ret

fcmpeS:
    fcmpe s0, s1
    mrs x0, NZCV
    ret

fccmpS:
    fccmp s0, s1, #0, EQ
    mrs x0, NZCV
    ret

fccmpeS:
    fccmpe s0, s1, #0, EQ
    mrs x0, NZCV
    ret

readfpsr:
    mrs x0, fpsr
    ret

readNZCV:
    mrs x0, NZCV
    ret

writeNZCV:
    msr NZCV, x0
    ret
C言語記述のmain関数

「通り一遍さわるだけの手抜きな」C言語記述のテスト駆動部です。メンドーなので

    • 単精度のみ、倍精度はパス(ARMv8.0には半精度ありませぬ)
    • FCCMP/FCCMPEは、EQ条件のときにフラグに反映、そうでなければ全フラグをクリアとする
    • FCMPE/FCCMPEに与えるNaN一族はqNaN(quiet NaN)1個だけ

など手抜きを重ねているのですが、ケースが多くなってしまいました。

#include <stdio.h>
#include <stdint.h>
#include <math.h>

extern uint32_t fcmpS(float, float);
extern uint32_t fcmp0S(float);
extern uint32_t fcmpeS(float, float);
extern uint32_t fccmpS(float, float);
extern uint32_t fccmpeS(float, float);
extern uint32_t readfpsr();
extern uint32_t readNZCV();
extern void writeNZCV(uint32_t);

int main(void)
{
    uint32_t result;
    uint32_t fpsr;

    result = fcmpS(1.23f, 1.23f);
    printf ("fcmpS(1.23f, 1.23f):%08x\n", result);
    result = fcmpS(1.23f, 4.56f);
    printf ("fcmpS(1.23f, 4.56f):%08x\n", result);
    result = fcmpS(4.56f, 1.23f);
    printf ("fcmpS(4.56f, 1.23f):%08x\n", result);
    result = fcmpS(4.56f, 1.23f);
    printf ("fcmpS(4.56f, 1.23f):%08x\n", result);
    result = fcmpS(1.23f, NAN);
    fpsr = readfpsr();
    printf ("fcmpS (1.23f, NAN):%08x\n", result);
    printf ("fcmpS (1.23f, NAN) FPSR:%08x\n", fpsr);

    result = fcmpeS(1.23f, NAN);
    fpsr = readfpsr();
    printf ("fcmpeS(1.23f, NAN):%08x\n", result);
    printf ("fcmpeS(1.23f, NAN) FPSR:%08x\n", fpsr);

    result = fcmp0S(1.23f);
    printf ("fcmp0S(1.23f) :%08x\n", result);
    result = fcmp0S(0.0f);
    printf ("fcmp0S(0.0f)  :%08x\n", result);
    result = fcmp0S(-1.23f);
    printf ("fcmp0S(-1.23f):%08x\n", result);

    writeNZCV(0x40000000);
    result = fccmpS(4.56f, 1.23f);
    printf ("ZERO fccmpS(4.56f, 1.23f):%08x\n", result);
    writeNZCV(0x00000000);
    result = fccmpS(4.56f, 1.23f);
    printf ("NON-ZERO fccmpS(4.56f, 1.23f):%08x\n", result);

    writeNZCV(0x40000000);
    result = fccmpeS(4.56f, NAN);
    printf ("ZERO fccmpeS(4.56f, NAN):%08x\n", result);
    writeNZCV(0x00000000);
    result = fccmpeS(4.56f, NAN);
    printf ("NON-ZERO fccmpeS(4.56f, NAN):%08x\n", result);

    return 0;
}
ビルドして実行

実行結果が以下に。NVCZは、イコールならば0x60000000、GTで0x20000000、LTで0x80000000が返ります。また、浮動小数点例外を許可していないので、例外発生条件になった場合、FPSRの最下位IOCビットが立つだけです。FCMP_results

メンドーだけれども、1個1個確かめました。OK。大丈夫か?

ぐだぐだ低レベルプログラミング(115)ARM64(AArach64)FMAX、FMIN  へ戻る

ぐだぐだ低レベルプログラミング(117)ARM64(AArach64)FCSEL へ進む