前回は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の順番がひっくり返っております。
リバース系、あんまりなネーミングだけれども、ちゃんと動いておりますぞ。