
前回は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個の命令ニーモニックです。
-
- RBIT、 Bitwise reverse
- MVN、 Bitwise NOT
- NOT、 Bitwise NOT
- REV16、 Reverse elements in 16-bit halfwords
- REV32、 Reverse elements in 32-bit words
- 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命令。バイト内のビット順序がリバースされとります。
つづいて、MVNとNOT命令。各ビットの値が反転されてます。MVNと殊更に別ニーモニック使ってますが、NOTの別名なので結果はまったく同じ。
REV16は00と01という塩梅で隣あう2つのバイト順がリバースされてます。REV32は00が03、01が02、02が01、03が00へリバース。REV64だと00から07の順番がひっくり返っております。
リバース系、あんまりなネーミングだけれども、ちゃんと動いておりますぞ。