毎度ですがA64の命令多すぎ。今回練習するのはSIMDのシフト命令です。符合付/符号無、サチュレーションの有無、丸めの有無で2の3乗、合計8種のニーモニックが存在します。そしてニーモニック上はLEFTと読めるので左シフトだけかと思えば「負の左シフトは右シフト」ということで右シフトも出来。でもこれだけじゃなかったんだ。
※「ぐだぐだ低レベルプログラミング」投稿順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個のニーモニックを演習してみました。これだって少なくはないですが、今回のシフト演算はニーモニック8個です。内訳はこんな感じ。
操作 | 符号付 | 符号無 |
---|---|---|
サチュレーション左シフト | SQSHL | UQSHL |
サチュレーション左シフト丸め | SQRSHL | UQRSHL |
左シフト | SSHL | USHL |
左シフト丸め | SRSHL | URSHL |
ニーモニック的には左シフトばかりに見えますが、上記命令のシフト回数はソース第2オペランドのレジスタで指定です。先ほど書いたとおり「負の左シフトは右シフト」というロジックなんであります。引数によっては右シフトにもなります。ややこしい?
なお、ソース第1オペランドの内容を、ソース第2オペランドで指定するビット数シフトしてデスティネーションに書き込む形です。
SIMDレジスタ的にはバイト、ハーフワード、ワード、ダブルワードのSIMD要素幅と、レジスタ幅64ビット/128ビットの選択が可能なので計8種類の「幅」に対応してます。
命令セットは上記の部分は「対称で綺麗」です。符合付か符号無か、サチューレーションするかしないか、丸めるか丸めないか。しかし「騙されて」はいけませぬぞ。この部分が綺麗な対称であるだけで、まだシフト系の命令は沢山隠れているのであります。引数がレジスタ指定でないものが。まあ今回は練習しないので流しますが。。。
実験につかったアセンブリ言語記述の被テスト関数
例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下に。例によってメンドクセーので、バイト幅エレメントの半幅レジスタのみで実習をしてみます。
.globl sqshl8V, uqshl8V, sqrshl8V, uqrshl8V, sshl8V, ushl8V, srshl8V, urshl8V .text .balign 4 sqshl8V: ld1 {v1.8B, v2.8B}, [x0], #16 sqshl v0.8B, v1.8B, v2.8B st1 {v0.8B}, [x0] ret uqshl8V: ld1 {v1.8B, v2.8B}, [x0], #16 uqshl v0.8B, v1.8B, v2.8B st1 {v0.8B}, [x0] ret sqrshl8V: ld1 {v1.8B, v2.8B}, [x0], #16 sqrshl v0.8B, v1.8B, v2.8B st1 {v0.8B}, [x0] ret uqrshl8V: ld1 {v1.8B, v2.8B}, [x0], #16 uqrshl v0.8B, v1.8B, v2.8B st1 {v0.8B}, [x0] ret sshl8V: ld1 {v1.8B, v2.8B}, [x0], #16 sshl v0.8B, v1.8B, v2.8B st1 {v0.8B}, [x0] ret ushl8V: ld1 {v1.8B, v2.8B}, [x0], #16 ushl v0.8B, v1.8B, v2.8B st1 {v0.8B}, [x0] ret srshl8V: ld1 {v1.8B, v2.8B}, [x0], #16 srshl v0.8B, v1.8B, v2.8B st1 {v0.8B}, [x0] ret urshl8V: ld1 {v1.8B, v2.8B}, [x0], #16 urshl v0.8B, v1.8B, v2.8B st1 {v0.8B}, [x0] ret
C言語記述のmain関数
上記のアセンブリ言語関数を呼び出すmain関数が以下に。これまたいつもの通りの手抜きで前回ソースのチョイ変デス。再度お断りすると sが頭についているニーモニックは符合付なのですが、C言語レベルでは全てuint8_t引数に対して操作させてます(どうせアセンブラにはCのレベルなど関係ねー。)
#include <stdio.h> #include <stdint.h> #define MAXMEM (24) uint8_t TargetMEM[MAXMEM]; extern void sqshl8V(uint8_t *); extern void uqshl8V(uint8_t *); extern void sqrshl8V(uint8_t *); extern void uqrshl8V(uint8_t *); extern void sshl8V(uint8_t *); extern void ushl8V(uint8_t *); extern void srshl8V(uint8_t *); extern void urshl8V(uint8_t *); void initTGT() { TargetMEM[0] =0x7F; TargetMEM[1] =0xFF; TargetMEM[2] =0x02; TargetMEM[3] =0x01; TargetMEM[4] =0x7F; TargetMEM[5] =0xFF; TargetMEM[6] =0x02; TargetMEM[7] =0x01; TargetMEM[8] =0x01; TargetMEM[9] =0x01; TargetMEM[10]=0x01; TargetMEM[11]=0x01; TargetMEM[12]=0xFF; TargetMEM[13]=0xFF; TargetMEM[14]=0xFF; TargetMEM[15]=0xFF; } void dumpTGT(const char *arg) { printf("%s\n", arg); for (int i=0; i < 8; i++) { printf("%02d: 0x%02x opr 0x%02x -> 0x%02x \n", i, TargetMEM[i], TargetMEM[i+8], TargetMEM[i+16]); } } int main(void) { initTGT(); sqshl8V(TargetMEM); dumpTGT("sqshl"); uqshl8V(TargetMEM); dumpTGT("uqshl"); sqrshl8V(TargetMEM); dumpTGT("sqrshl"); uqrshl8V(TargetMEM); dumpTGT("uqrshl"); sshl8V(TargetMEM); dumpTGT("sshl"); ushl8V(TargetMEM); dumpTGT("ushl"); srshl8V(TargetMEM); dumpTGT("srshl"); urshl8V(TargetMEM); dumpTGT("urshl"); return 0; }
実機実行結果の確認
以下のようにしてビルドして実行しています。
$ gcc -g -O0 sqshl.c sqshl.s $ ./a.out
標準出力に「ダラダラ」現れた結果を、上下左右比べ易いように折りたたんだものが以下に。
上下を見比べると符号付き、符号無の挙動の差が、左右4列を見比べると、サチュレーションの有無、丸めの有無の挙動の差が見えるかと。こうして見比べればその差は明らかだけれど、そうじゃなければ見逃してる?