ぐだぐだ低レベルプログラミング(164)ARM64(AArach64)SIMD即値命令

Joseph Halfmoon

前回はSIMDの転置(transpose)命令に「絶対自分じゃ思いつかね~」と感心しました。今回はSIMDでも即値(イミーディエイト)をソースにとる命令群です。たった8ビットなんだけれどもその効果たるや意外と複雑?中でも8ビット即値を浮動小数にエンコードしてロードするFMOV命令にはちょいとてこずりましたぞ。

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

今回は「第1オペランドで指定したSIMDレジスタ(ベクトル)中の全要素に第2オペランドで指定した単一の8ビット即値を作用させ、SIMDレジスタに書き戻す命令」群です。ニーモニック的には以下の5個です。

    • BIC
    • ORR
    • MOVI
    • MVNI
    • FMOV

ニーモニック的には既に何度も練習したことがあるような気がします。けれどもSIMDの即値命令としては初めてのハズ。SIMDレジスタの要素のビット幅は即値の8ビットより広いのがほぼほぼ当然なので、8ビットをどう当てはめるのか、かなり工夫されている感じの命令です。BIC、ORR、MOVI、MVNIの4命令については第3オペランドに左シフト量をとり(シフト量は0、8、16、24という8ビット単位です)即値は8ビットしかないのだけれども32ビットワードの最上位バイトまで作用させられるようになってます。

BICはビット・クリア、ORRはビット・セットとして要素の一部ビットのみの操作に使用できます。一方MOVI、MVNI(反転MOV)は、即値ロードなので要素のビット幅の全てを使い、「即値をシフトした」値を書き込みます。

最後のFMOV命令は、第2オペランドにとる「8ビット幅の即値からエンコードされる」浮動小数をSIMDレジスタの各要素にロードする命令です。皆さまご存じのとおり単精度の浮動小数の場合、浮動小数点数全体としては以下のビット数が必要です。

    • 符号ビット1ビット
    • 指数部8ビット(値は下駄を履いている)
    • 仮数部23ビット(暗黙のビットあり)

8ビット即値では上記全部を埋められないので、「ある決まり」に従って上記ビットにはめ込むことになってます。

馬鹿なので、最初、第2引数を #127 とか「整数の即値」で書いてアセンブラに文句を言われました。

Error: invaild floating-point constant at operand 2

というエラー発生です。よくよく調べてみると機械語命令に埋め込まれる即値は8ビットなのですが、アセンブラで記述する第2オペランドは 小数点を含む記法で書けたのです。#1.0 と書けば浮動小数点数 1.0 がロードされると。

しかし、#1.45 とかテキトーな数を記述するとやっぱり怒られます。こんな感じ。InvalidFPconst

そのココロは、「8ビットの即値で指定可能な」浮動小数を第2引数に記さねばならんのでした。たとえば

#1.4375

ならばアセンブラは問題なく受け入れてくれます。でも、お惚け老人には8ビット即値に対応する浮動小数を計算するのはツライ、とても無理、と思ったらいつも眺めている命令セットマニュアルの以下の表に「一覧」がありました。

Table C2-2 Floating-point constant values

この表に掲載されている値のみがFMOVが第2引数に受け入れ可能な数値だと。表(符号マイナスも可)にない即値を書くとエラーになります。メンドクセー命令だな、ほんと。

実験につかったアセンブリ言語記述の被テスト関数

例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下に。

.globl	bic4V, orr4V, movi4V, mvni4V, fmov4V 
.text
.balign	4

bic4V:
    ld1  {V0.4S}, [x0]
    bic  v0.4S, #15, LSL #16
    st1  {v0.4S}, [x0]
    ret

orr4V:
    ld1  {V0.4S}, [x0]
    orr  v0.4S, #15, LSL #24
    st1  {v0.4S}, [x0]
    ret

movi4V:
    ld1  {V0.4S}, [x0]
    movi v0.4S, #129, LSL #8
    st1  {v0.4S}, [x0]
    ret

mvni4V:
    ld1  {V0.4S}, [x0]
    mvni v0.4S, #129, LSL #24
    st1  {v0.4S}, [x0]
    ret

fmov4V:
    ld1  {V0.4S}, [x0]
    fmov v0.4S, #1.4375
    st1  {v0.4S}, [x0]
    ret
C言語記述のmain関数

上記のアセンブリ言語関数を呼び出すmain関数が以下に。

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

#define MAXMEM	(4)
uint32_t TargetMEM[MAXMEM];
float TargetMEM2[MAXMEM];

extern void bic4V(uint32_t *);
extern void orr4V(uint32_t *);
extern void movi4V(uint32_t *);
extern void mvni4V(uint32_t *);
extern void fmov4V(float *);

void initTGT() {
    TargetMEM[0]   = 0x12345678;
    TargetMEM[1]   = 0x9ABCDEF0;
    TargetMEM[2]   = 0x5A5A5A5A;
    TargetMEM[3]   = 0xFFFFFFFF;
}

void initTGT2() {
    TargetMEM2[0]   = 1.0f;
    TargetMEM2[1]   = 2.0f;
    TargetMEM2[2]   = 0.5f;
    TargetMEM2[3]   = 0.25f;
}

void dumpTGT(const char *arg) {
    printf("%s\n", arg);
    for (int i=0; i<4; i++) {
        printf("%02d: %08x\n", i, TargetMEM[i]);
    }
}

void dumpTGT2(const char *arg) {
    printf("%s\n", arg);
    for (int i=0; i<4; i++) {
        printf("%02d: %f\n", i, TargetMEM2[i]);
    }
}

int main(void) {
    initTGT();
    bic4V(TargetMEM);
    dumpTGT("bic v0.4S, #15, LSL #16");

    initTGT();
    orr4V(TargetMEM);
    dumpTGT("orr v0.4S, #15, LSL #24");

    initTGT();
    movi4V(TargetMEM);
    dumpTGT("movi v0.4S, #129, LSL #8");

    initTGT();
    mvni4V(TargetMEM);
    dumpTGT("mvni v0.4S, #129, LSL #24");

    initTGT2();
    fmov4V(TargetMEM2);
    dumpTGT2("fmov v0.4S, #1.4375");

    return 0;
}
実機実行結果の確認

以下のようにしてビルドして実行しています。

$ gcc -g -O0 simdImm.c simdImm.s
$ ./a.out

実行結果が以下に。

simdImmResults期待どおりね。FMOVにはちょっとてこずったけど、表さえあれば簡単じゃん。

ぐだぐだ低レベルプログラミング(163)ARM64(AArach64)SIMD 転置命令 へ戻る

ぐだぐだ低レベルプログラミング(165)ARM64(AArach64)SIMD即値シフト へ進む