ぐだぐだ低レベルプログラミング(177)ARM64(AArach64)SIMD 表引き命令群

Joseph Halfmoon

今回は「表引き」命令群です。「群」といっても2命令だけ。SIMD(ベクトル)ソースレジスタのバイト要素をインデックスとして、複数のSIMDレジスタを「バイト・テーブル」としてアクセスし、引いた結果をデスティネーションのSIMDレジスタに書き込むという命令です。1度に最大16個の表引きをできるもの。単純だけれど強力?

※「ぐだぐだ低レベルプログラミング」投稿順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要素分」を図にすると以下のようです。以下ではソースの最下位要素に0x11というインデックスが書き込まれていた場合に、4個のベクトルレジスタで構成されたテーブルを引き、そこで見つかった0xFAという値をデスティネーションの最下位要素に書き込んでいます。tbl

SIMDなので、同時にソースの全16要素に対して同様な操作が行われます。単純だけれどもメンドイ表引きを一気に出来るので強力っす。

以下の2命令があります。

    • TBL
    • TBX

その違いはインデックス値がテーブルの中を指さない場合の動作です。TBL命令の場合、インデックスがテーブルの中に無かった場合、結果0を返しますが、TBX命令の場合はデスティネーションの元の値が維持されます。TBXを使えば、複数のテーブルレジスタを切り替えながら表引きすることも可能なハズ。

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

例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下に。以下ではSIMD(ベクトル)レジスタ幅は128ビット、テーブルレジスタは2本という設定です。

.globl	tbl32V, tbx32V 
.text
.balign	4

tbl32V:
    ld1  {v0.16B, v1.16B, v2.16B, v3.16B}, [x0]
    tbl  v0.16B, {v1.16B, v2.16B}, v3.16B
    st1  {v0.4S}, [x0]
    ret

tbx32V:
    ld1  {v0.16B, v1.16B, v2.16B, v3.16B}, [x0]
    tbx  v0.16B, {v1.16B, v2.16B}, v3.16B
    st1  {v0.4S}, [x0]
    ret
C言語記述のmain関数

上記のアセンブリ言語関数を呼び出すmain関数が以下に。符号無バイト操作の命令どもですが、バイトを並べると書くのがメンドイという理由だけで、C言語上は符号無32ビットで記述してます。

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

#define MAXMEM	(16)

uint32_t TargetMEM[MAXMEM];

extern void tbl32V(uint32_t *);
extern void tbx32V(uint32_t *);

void initTGT() {
    TargetMEM[0]  = 0xFFFFFFFF;
    TargetMEM[1]  = 0xFFFFFFFF;
    TargetMEM[2]  = 0xFFFFFFFF;
    TargetMEM[3]  = 0xFFFFFFFF;
    TargetMEM[4]  = 0x87654321;
    TargetMEM[5]  = 0xFEDCBA87;
    TargetMEM[6]  = 0xA5A5A5A5;
    TargetMEM[7]  = 0x5A5A5A5A;
    TargetMEM[8]  = 0x11111111;
    TargetMEM[9]  = 0x22222222;
    TargetMEM[10] = 0x33333333;
    TargetMEM[11] = 0x44444444;
    TargetMEM[12] = 0x01000001;
    TargetMEM[13] = 0x04050607;
    TargetMEM[14] = 0x1C181410;
    TargetMEM[15] = 0x1F022021;
}

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

int main(void) {

    initTGT();
    tbl32V(TargetMEM);
    dumpTGT("tbl");

    initTGT();
    tbx32V(TargetMEM);
    dumpTGT("tbx");

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

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

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

標準出力に現れた結果が以下に。赤枠の2バイトは、tbl命令においてインデックスがテーブル内を指していないところで、結果は0。黄色枠の2バイトは同じ「ハズレ」インデックスのtbx命令での結果です。0xffは元の値です。tblResults

ちゃんと表引きできているみたい。あたりまえか。

ぐだぐだ低レベルプログラミング(176)ARM64(AArach64)SIMD ペア操作群2 へ戻る