ぐだぐだ低レベルプログラミング(112)ARM64(AArach64)積和は”fused”

Joseph Halfmoon

今回から浮動小数の積和演算に入ります。「掛けた結果を足しこむ」積和演算は、積分というかコンボリューションというか、信号処理かAIか、近代的な各種アルゴリズムで多用される演算です。何万回どころか何億回も。そのような計算を高速化してくれる積和演算命令ですが、普通に掛けてから足すのとは結果が微妙に違うことがあると。ホントか?

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

Floating-point fused Multiply-Add

スカラー浮動小数の積和演算命令4種の「代表」FMADD命令をみると上記タイトルのように “fused” という単語が挟まってます。これを端的に説明するならば、

    • 普通のMultiply(乗算)をやって普通のAdd(加算)を2命令で処理するときには、各命令の末尾で「丸め」をする
    • fusedな積和演算命令では、乗算から加算の間は「丸めをしない」でおいて最後に1回だけ丸めを行う

という感じでしょうかね。例えば単精度浮動小数のフォーマット的には仮数部は物理的には23ビット(先頭の暗黙の1ビットをいれて24ビット相当)でレジスタに格納する時点では「丸め」を通してこの幅に収めなければいけないのだけれども、演算途中の実装上は23ビットよりも下の桁まで存在しておる、と(そじゃないと丸められないし。)掛け算やって足し算を続けてやるなら途中の丸めなど飛ばして「やっちゃえ」というのが fused みたいデス。その方が回路を通過する速度も速いし、精度もあがるっと。

この「精度もあがる」のところが乗算+加算の2命令との差を生み出す筈。まあ仮数部の下のビットの話なので微妙な差なんだけれども、なにせ1つの値を計算するために積和命令は何万回どころか何億回も繰り返すことがあるので塵も積もれば山となるのでありました。

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

いつものように手抜きな、関数プロローグもエピローグもない1命令1関数スタイルです。fused計算である積和演算命令 fmadd と、普通のfmul + fadd の2命令を比べるためのもの。なお、丸めについてはfpcr指定の「成り行き」です。デフォルトでは最近接awayの筈。

.globl	fmaddS, fmuladdS
.text
.balign	4

fmaddS:
    fmadd s0, s1, s2, s3
    ret

fmuladdS:
    fmul s0, s1, s2
    fadd s0, s0, s3
    ret
C言語記述のmain関数

これまた手抜きなC言語記述のテスト駆動部が以下に。今回はたった2例、fused計算の fmadd と普通の計算の fmul+fadd の差が見えるケース、両者が完全に一致するケースの2つです。ただね1回の計算では両者の差は微妙。仮数部の最後のビットが違うだけなのでprintfで10進小数に変換してしまうとよくわかりません。そこで16進表記を併記してます。

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

extern float fmaddS(float, float, float, float);
extern float fmuladdS(float, float, float, float);

int main(void)
{
    float fin;
    union {
        float s;
        uint32_t u;
    } u32a, u32b;

    fin = 0.0009992f;
    u32a.s = fmaddS  (0.0f, fin, 1.1f, 0.000003f);
    u32b.s = fmuladdS(0.0f, fin, 1.1f, 0.000003f);
    printf ("unmatch: %2.8f madd=%2.8f(%08x) mul_add=%2.8f(%08x)\n", fin, u32a.s, u32a.u, u32b.s, u32b.u);

    fin = 0.0009993f;
    u32a.s = fmaddS  (0.0f, fin, 1.1f, 0.000003f);
    u32b.s = fmuladdS(0.0f, fin, 1.1f, 0.000003f);
    printf ("match:   %2.8f madd=%2.8f(%08x) mul_add=%2.8f(%08x)\n", fin, u32a.s, u32a.u, u32b.s, u32b.u);

    return 0;
}
実行結果

ビルドして実行したものが以下に。%fで10進浮動小数としてprintfした値では差異は見えないですが、()内に併記してある16進数表記をご覧くだされ。赤線で印をつけたところ違ってますやん。fused_result

差は微妙、でもま、この計算を1億回も繰り返して和を積算していったら差はデカい。ホントか?

ぐだぐだ低レベルプログラミング(111)ARM64(AArach64)FRINTx に戻る

ぐだぐだ低レベルプログラミング(113)ARM64(AArach64)積和演算4種の違い へ進む