
毎度ですが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列を見比べると、サチュレーションの有無、丸めの有無の挙動の差が見えるかと。こうして見比べればその差は明らかだけれど、そうじゃなければ見逃してる?