ぐだぐだ低レベルプログラミング(148)ARM64(AArach64)SIMD 反転系

Joseph Halfmoon

前回は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

御勝手分類、SIMD「反転系」

今回、一度に練習するのにまとめてしまったのは以下の6個の命令ニーモニックです。

    1. RBIT、 Bitwise reverse
    2. MVN、 Bitwise NOT
    3. NOT、 Bitwise NOT
    4. REV16、 Reverse elements in 16-bit halfwords
    5. REV32、 Reverse elements in 32-bit words
    6. REV64、 Reverse elements in 64-bit doublewords

「1」のRBIT命令は、1バイト(8ビット)の中のビットの並び順を反転(リバース)するものです。MSBがLSBにという感じで以下同文。

「2」と「3」はいずれも位置は変えず、各ビットが0であれば1、1であれば0と反転(NOT)をとるものです。なおMVNはエイリアスであり、MVNとNOTは同じ命令動作です。

一方「4」「5」「6」は規定の範囲の中の要素内の順序を反転(リバース)するものです。「4」のREV16であれば、16ビット幅のSIMD要素の中で8ビットのバイトの上下を入れ替えます。REV32の場合、32ビット幅のSIMD要素の中で、8ビットあるいは16ビットの「サブ要素」の順序を反転します。REV64も以下同文ね。

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

いつものように手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下です。いずれもSIMDレジスタ幅はフル幅半分の64ビットのみ使用。REV32やREV64命令にはバイト幅以外の「サブ要素」を扱うこともできますが、今回は全部バイトっす。

.globl	rbit8V, mvn8V, not8V, rev16BV, rev32BV, rev64BV 
.text
.balign	4

rbit8V:
    ld1  {v1.8B}, [x0], #8
    rbit  v0.8B,  v1.8B
    st1  {v0.8B}, [x0]
    ret

mvn8V:
    ld1  {v1.8B}, [x0], #8
    mvn  v0.8B,  v1.8B
    st1  {v0.8B}, [x0]
    ret

not8V:
    ld1  {v1.8B}, [x0], #8
    not  v0.8B,  v1.8B
    st1  {v0.8B}, [x0]
    ret

rev16BV:
    ld1  {v1.8B}, [x0], #8
    rev16  v0.8B,  v1.8B
    st1  {v0.8B}, [x0]
    ret

rev32BV:
    ld1  {v1.8B}, [x0], #8
    rev32  v0.8B,  v1.8B
    st1  {v0.8B}, [x0]
    ret

rev64BV:
    ld1  {v1.8B}, [x0], #8
    rev64  v0.8B,  v1.8B
    st1  {v0.8B}, [x0]
    ret
C言語記述のmain関数

上記のアセンブリ言語関数を呼び出すmain関数が以下に。符号付き整数に対する処理でもCのレベルでは全てuint8_t型で書いているもの。

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

#define MAXMEM	(16)

uint8_t TargetMEM[MAXMEM];


extern void rbit8V(uint8_t *);
extern void mvn8V(uint8_t *);
extern void not8V(uint8_t *);
extern void rev16BV(uint8_t *);
extern void rev32BV(uint8_t *);
extern void rev64BV(uint8_t *);

void initTGT() {
    TargetMEM[0]  = 0x00;
    TargetMEM[1]  = 0x01;
    TargetMEM[2]  = 0x02;
    TargetMEM[3]  = 0x7F;
    TargetMEM[4]  = 0xFF;
    TargetMEM[5]  = 0xFE;
    TargetMEM[6]  = 0xFD;
    TargetMEM[7]  = 0x80;
    TargetMEM[8]  = 0x00;
    TargetMEM[9]  = 0x00;
    TargetMEM[10] = 0x00;
    TargetMEM[11] = 0x00;
    TargetMEM[12] = 0x00;
    TargetMEM[13] = 0x00;
    TargetMEM[14] = 0x00;
    TargetMEM[15] = 0x00;
}

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

int main(void) {
    initTGT();
    rbit8V(TargetMEM);
    dumpTGT("rbit");

    initTGT();
    mvn8V(TargetMEM);
    dumpTGT("mvn");

    initTGT();
    not8V(TargetMEM);
    dumpTGT("not");

    initTGT();
    rev16BV(TargetMEM);
    dumpTGT("rev16");

    initTGT();
    rev32BV(TargetMEM);
    dumpTGT("rev32");

    initTGT();
    rev64BV(TargetMEM);
    dumpTGT("rev64");

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

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

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

実行結果が以下に。まずはRBIT命令。バイト内のビット順序がリバースされとります。rbit

つづいて、MVNとNOT命令。各ビットの値が反転されてます。MVNと殊更に別ニーモニック使ってますが、NOTの別名なので結果はまったく同じ。simd_mvn_not

REV16は00と01という塩梅で隣あう2つのバイト順がリバースされてます。REV32は00が03、01が02、02が01、03が00へリバース。REV64だと00から07の順番がひっくり返っております。simd_rev

リバース系、あんまりなネーミングだけれども、ちゃんと動いておりますぞ。

ぐだぐだ低レベルプログラミング(147)ARM64(AArach64)SIMD 符号系 へ戻る

ぐだぐだ低レベルプログラミング(149)ARM64(AArach64)SIMD 逆数系 へ進む