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

Joseph Halfmoon

まだ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オペランドの対応位置の各ビット間の演算を図にしてみました。BIT_OPR_SIMDa

台形のハコはマルチプレクサです。

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

例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下に。全て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)。and_bic

お次は、BIF(bitwise bit if false)とBIT(bitwise bit if true)。bif_bit

最後にBSL。bsl

予定通りの処理が出来ているみたいだけれども。単純なビット演算だけれどSIMDレジスタの全幅になるだけで目が回る気がするよ、あたしゃ。

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

ぐだぐだ低レベルプログラミング(134)ARM64(AArach64) SIMD frecps へ進む