
まだSIMDのMOV命令は残っているのですが、今回はさっさと先に進みます。言ってもしょうがないけどA64の命令多すぎ。特にSIMD命令多すぎ。今回実験してみるのはSIMDのbit操作関係の命令群です。ビット操作なので要素は記述の形式的で、実際は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
※積み残したMOV命令は、SIMDレジスタ上のベクトル要素からSIMDレジスタ上のスカラーへの転送です。A64の場合、SIMDレジスタはスカラーの浮動小数点レジスタを兼ねているので、当然必要な操作なんだけれど地味。いいのか?
SIMD(ベクトル)レジスタのビット操作命令群
今回練習してみる命令群は以下です。ベクトル命令なので、オペランドにベクトルレジスタをとりますが、許される要素は8B(バイト。SIMDレジスタのビット幅は64ビット)か16B(バイト。SIMDレジスタのビット幅は128ビット)です。Bのアセンブラ記述は操作対象のSIMDレジスタのビット幅を決めるためのもので、実際の演算はビット単位となります。
どの命令もソース2つ、デスティネーション1つの3オペランド命令です。3オペランドの対応位置の各ビット間の演算を図にしてみました。
台形のハコはマルチプレクサです。
実験につかったアセンブリ言語記述の被テスト関数
例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下に。全て128ビット幅としています。
.globl andv, bicv, bifv, bitv, bslv
.text
.balign 4
andv:
ld1 {v0.16B, v1.16B, v2.16B}, [x0], #48
and v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
bicv:
ld1 {v0.16B, v1.16B, v2.16B}, [x0], #48
bic v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
bifv:
ld1 {v0.16B, v1.16B, v2.16B}, [x0], #48
bif v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
bitv:
ld1 {v0.16B, v1.16B, v2.16B}, [x0], #48
bit v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
bslv:
ld1 {v0.16B, v1.16B, v2.16B}, [x0], #48
bsl v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
C言語記述のmain関数
上記のアセンブリ言語関数を呼び出すmain関数が以下に。ビット単位で結果の総当たりを1回の操作で求めるならば1バイトあれば足りるのですが、ベクトル命令なので幅広のままで処理してます。無意味に冗長だけどベクトルだということだけは身に染みる?
#include <stdio.h>
#include <stdint.h>
#define MAXMEM (16)
uint32_t TargetMEM[MAXMEM];
extern void andv(uint32_t *);
extern void bicv(uint32_t *);
extern void bifv(uint32_t *);
extern void bitv(uint32_t *);
extern void bslv(uint32_t *);
void initTGT(uint32_t* c) {
for (int i=0; i < 4; i++) {
TargetMEM[i*4+0] = c[i];
TargetMEM[i*4+1] = c[i];
TargetMEM[i*4+2] = c[i];
TargetMEM[i*4+3] = c[i];
}
}
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 c[4];
c[0]=0x00000000;
c[1]=0x0F0F0F0F;
c[2]=0xA5A5A5A5;
c[3]=0xFFFFFFFF;
initTGT(c);
dumpTGT("Before and vector");
andv(TargetMEM);
dumpTGT("After and vector");
c[0]=0x00000000;
c[1]=0x0F0F0F0F;
c[2]=0xA5A5A5A5;
c[3]=0xFFFFFFFF;
initTGT(c);
dumpTGT("Before bic vector");
bicv(TargetMEM);
dumpTGT("After bic vector");
c[0]=0x0000FFFF;
c[1]=0x0F0F0F0F;
c[2]=0xA5A5A5A5;
c[3]=0xFFFFFFFF;
initTGT(c);
dumpTGT("Before bif vector");
bifv(TargetMEM);
dumpTGT("After bif vector");
c[0]=0x0000FFFF;
c[1]=0x0F0F0F0F;
c[2]=0xA5A5A5A5;
c[3]=0xFFFFFFFF;
initTGT(c);
dumpTGT("Before bit vector");
bitv(TargetMEM);
dumpTGT("After bit vector");
c[0]=0x0000FFFF;
c[1]=0x0F0F0F0F;
c[2]=0xA5A5A5A5;
c[3]=0xFFFFFFFF;
initTGT(c);
dumpTGT("Before bsl vector");
bslv(TargetMEM);
dumpTGT("After bsl vector");
return 0;
}
実機実行結果の確認
以下のようにしてビルドして実行しています。
$ gcc -g -O0 bit.c bit.s $ ./a.out
標準出力に「ダラダラ」現れる結果を折りたたんで、見やすいように関係個所を枠で囲ってみました。
まずは、ANDとBIC(bitwise bit clear)。
お次は、BIF(bitwise bit if false)とBIT(bitwise bit if true)。
予定通りの処理が出来ているみたいだけれども。単純なビット演算だけれどSIMDレジスタの全幅になるだけで目が回る気がするよ、あたしゃ。
