ぐだぐだ低レベルプログラミング(132)ARM64(AArach64)整数/SIMD間転送

Joseph Halfmoon

前回までSIMD(ベクトル)レジスタ間での転送を練習してきましたが、今回は汎用(整数)レジスタとSIMDレジスタ間での転送を練習してみます。前回も登場したINS命令とDUP命令がここでも登場します。またUMOVとかSMOVとか一味違う奴らも登場。例によってMOVはエイリアスなんだけれどここでは幅を利かせてるみたい。

※「ぐだぐだ低レベルプログラミング」投稿順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(ベクトル)レジスタ間の転送

整数レジスタはWx(xはレジスタ番号)と呼ばれれば32ビット幅、Xxと呼ばれれば64ビット幅です。SIMDレジスタとはビット幅が異なるので、相互の転送は対称にはなりえません。非対称ね。ざっくりとまとめると以下のようになります。

    1. 整数レジスタの内容をSIMDレジスタの要素にDuplicate
    2. 整数レジスタの内容をSIMDレジスタの要素にInsert
    3. SIMDレジスタの要素を整数レジスタにMove

1は整数レジスタの内容を1要素としてみて、SIMDレジスタに複製して詰め込むもの。前回も登場したDUPというニーモニックのソースに整数レジスタを指定することで行えます。ただし、Duplicateするデスティネーションのビット幅は、バイト(B)、ハーフワード(H)、ワード(S)、ダブルワード(D)と選択の余地あり。また、SIMDレジスタも64ビット幅とするか128ビット幅とするかの選択もあり。デスティネーション側の要素指定はビット幅で7種類ということになります。メンドクセー。

2も1同様にビット幅の選択肢があります。さらに特定要素を1個書き換えることになるので要素の指定も必要です。ニーモニックはINSですが、MOVと書いても良いみたいです。いつものエイリアスね。でもマニュアル読むとこのケースでディスアセンブラはMOVとディスアセンブルするらしいのでご優待です。どっちがエイリアスなんだかなあ。知らんけど。

3は、1,2と逆方向でSIMDレジスタの要素を整数レジスタに転送します。整数レジスタのビット幅はWかXなのだけれど、転送元のSIMD要素のビット幅はいろいろです。整数扱いだけれど、符合拡張したいケースもあると思われるので、ニーモニックは2種類、符号無のUMOVと符号付のSMOVが用意されとります。また、例によってUMOVの一部のオペランド組み合わせはMOVと記述してもよいと(またまたエイリアスね。)

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

例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下に。ビット幅はいろいろとれるので、今回は一番コマケー奴ということでバイトにしてみました。

.globl	ginsv, gdupv, gumov, gsmov
.text
.balign	4

gdupv:
    ld1  {v0.16B}, [x0], #16
    dup  v0.16B,  w1
    st1  {v0.16B}, [x0]
    ret

ginsv:
    ld1  {v0.16B}, [x0], #16
    ins  v0.B[2], w1
    st1  {v0.16B}, [x0]
    ret

gumov:
    ld1  {v0.16B}, [x0]
    umov  w0, v0.B[1]
    ret

gsmov:
    ld1  {v0.16B}, [x0]
    smov  w0, v0.B[1]
    ret
C言語記述のmain関数

上記のアセンブリ言語関数を呼び出すmain関数が以下に。例のとおりで前回のソースの「チョイ変」っす。メモリにテキトーな値を並べておいてSIMDレジスタにロード、汎用レジスタからdupとinsした結果を再びメモリにストアして眺めてます。umovとsmovについては読みだした結果を素直にレジスタに返して吟味?しております。

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

#define MAXMEM	(16)
uint32_t TargetMEM[MAXMEM];

extern void gdupv(uint32_t *, uint8_t);
extern void ginsv(uint32_t *, uint8_t);
extern uint32_t gumov(uint32_t *);
extern int32_t gsmov(uint32_t *);

void initTGT(uint32_t c) {
    for (int i=0; i < MAXMEM; i++) {
        TargetMEM[i] = c * (i+1);
    }
}

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

int main(void) {
    uint32_t resultU;
    int32_t  resultS;

    initTGT(0x1000);

    dumpTGT("Before dup GR to vector");
    gdupv(TargetMEM, 0x5A);
    dumpTGT("After  dup GR to vector");
    ginsv(&TargetMEM[8], 0x5A);
    dumpTGT("After  ins GR to vector");
    resultU = gumov(&TargetMEM[8]);
    printf("UMOV: %08x\n", resultU);
    resultS = gsmov(&TargetMEM[8]);
    printf("SMOV: %08x\n", resultS);
    return 0;
}
実験結果

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

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

標準出力に「ダラダラ」現れる結果を折りたたんで、見やすいように関係個所を枠で囲ってみました。results0

中央赤枠が バイト幅でDUPした結果です。ビッシリ0x5aという値が繰り返されておりますな。右下の青枠がバイト幅で要素[2]にINSした結果です。SIMDレジスタをロードしてストアしている青枠の中に、汎用レジスタからバイト幅INSした結果が赤枠0x5aとして現れとります。

一方、UMOVとSMOVでは、どちらも上記の8番目にダンプされている0x0000900の下から2バイト目の0x90を取り出して、汎用レジスタに転送しています。UMOVとSMOVで符合の始末の扱いが変わっているのが分かると思います。umovsmov

動作は明快、だと思うんですけど言葉で書くのはシンドイな。アセンブラで書きたい。書いてるだろ~。

ぐだぐだ低レベルプログラミング(131)ARM64(AArach64)DUP(ベクトル)へ戻る

ぐだぐだ低レベルプログラミング(133)ARM64(AArach64) SIMD bit操作 へ進む