
今回練習するのはSIMDの比較命令です。スカラー同士の比較であれば分岐のためですが、SIMDの場合は各要素の計算を「通すか否か」のマスク的なものの生成。今回対象は浮動小数比較でなく整数のみですが、いつものとおりA64の命令多すぎ。便利そうな命令は網羅するのがArmの行き方か。ミニマリストではないわいな。多分。
※「ぐだぐだ低レベルプログラミング」投稿順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の比較命令
A64の命令を「さらっと一通りなでる」だけのつもりなのですが、命令多すぎてすすみませぬ。今回はニーモニック数で9命令を練習するのですがそれで整数比較のみです。浮動小数はまたこんどね。ざっくりした表にまとめると以下の如し。
| 比較 | 符号付 | 符号無 |
|---|---|---|
| = | CMEQ | CMEQ |
| =0 | CMEQ | CMEQ |
| ≧ | CMGE | CMHS |
| ≧0 | CMGE | CMHS |
| > | CMGT | CMHI |
| >0 | CMGT | CMHI |
| ≦0 | CMLE | —- |
| <0 | CMLT | —- |
| bit test< | CMTST | CMTST |
整数型なので符合付と符号無あり。両方に適用できる命令もあれば、片方だけのものもあり。「グレーター」とか「レス」とか言ったら符合付、「ハイ」とか「セイム」とかいったら符合無っす。
比較条件を見てみると、対ゼロ比較、優遇されてます。ゼロ相手の比較であれば貴重なレジスタ1本使わずに比較できるっと。また、符合付の方が優遇されている感じ。対ゼロのみですが「レス・イコール」とか「レス・ザン」とかが存在します。符合無の方は「大なり」系しかないので、「小なり」系は条件ヒックリ返さないとならないのに。やるのはコンパイラ様の仕事だけど(アセンブラ書かなければ。)命令多すぎA64、使えるものは網羅するのだ。
実験に使ったアセンブリ言語記述の被テスト関数
例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下です。例によってSIMD要素はバイト、ハーフワード、ワード、ダブルワードととれるのに、バイトのみです。いろいろなケースを網羅するためにSIMDレジスタは128ビット幅(バイト16要素並列)としてます。
今回はニーモニック数で9ですが、ゼロ相手とそうでない場合でソースオペラントの取り方が違うので関数的には多くなりました。メンドイのでゼロ相手の時も「不要な」第2ソースまでロードしています。手抜き。
.globl cmeq16V, cmhs16V, cmge16V, cmhi16V, cmgt16V, cmleZ16V, cmltZ16V, cmtst16V, cmeqZ16V, cmgeZ16V, cmgtZ16V
.text
.balign 4
cmeq16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmeq v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
cmhs16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmhs v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
cmge16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmge v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
cmhi16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmhi v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
cmgt16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmgt v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
cmleZ16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmle v0.16B, v1.16B, #0
st1 {v0.16B}, [x0]
ret
cmltZ16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmlt v0.16B, v1.16B, #0
st1 {v0.16B}, [x0]
ret
cmtst16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmtst v0.16B, v1.16B, v2.16B
st1 {v0.16B}, [x0]
ret
cmeqZ16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmeq v0.16B, v1.16B, #0
st1 {v0.16B}, [x0]
ret
cmgeZ16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmge v0.16B, v1.16B, #0
st1 {v0.16B}, [x0]
ret
cmgtZ16V:
ld1 {v1.16B, v2.16B}, [x0], #32
cmgt v0.16B, v1.16B, #0
st1 {v0.16B}, [x0]
ret
C言語記述のmain関数
上記のアセンブリ言語関数を呼び出すmain関数が以下に。毎度のお断りですが、符合付操作も、C言語レベルでは全てuint8_t引数に見えます。
#include <stdio.h>
#include <stdint.h>
#define MAXMEM (48)
uint8_t TargetMEM[MAXMEM];
extern void cmeq16V(uint8_t *);
extern void cmeqZ16V(uint8_t *);
extern void cmhs16V(uint8_t *);
extern void cmge16V(uint8_t *);
extern void cmhi16V(uint8_t *);
extern void cmgt16V(uint8_t *);
extern void cmleZ16V(uint8_t *);
extern void cmltZ16V(uint8_t *);
extern void cmtst16V(uint8_t *);
extern void cmgeZ16V(uint8_t *);
extern void cmgtZ16V(uint8_t *);
void initTGT() {
TargetMEM[0] =0x00;
TargetMEM[1] =0x01;
TargetMEM[2] =0x7E;
TargetMEM[3] =0x7F;
TargetMEM[4] =0x80;
TargetMEM[5] =0x81;
TargetMEM[6] =0xFE;
TargetMEM[7] =0xFF;
TargetMEM[8] =0x00;
TargetMEM[9] =0x01;
TargetMEM[10]=0x7E;
TargetMEM[11]=0x7F;
TargetMEM[12]=0x80;
TargetMEM[13]=0x81;
TargetMEM[14]=0xFE;
TargetMEM[15]=0xFF;
TargetMEM[16]=0x01;
TargetMEM[17]=0x01;
TargetMEM[18]=0x01;
TargetMEM[19]=0x01;
TargetMEM[20]=0x01;
TargetMEM[21]=0x01;
TargetMEM[22]=0x01;
TargetMEM[23]=0x01;
TargetMEM[24]=0x81;
TargetMEM[25]=0x81;
TargetMEM[26]=0x81;
TargetMEM[27]=0x81;
TargetMEM[28]=0x81;
TargetMEM[29]=0x81;
TargetMEM[30]=0x81;
TargetMEM[31]=0x81;
}
void dumpTGT(const char *arg) {
printf("%s\n", arg);
for (int i=0; i < 16; i++) {
printf("%02d: 0x%02x opr 0x%02x -> 0x%02x \n", i, TargetMEM[i], TargetMEM[i+16], TargetMEM[i+32]);
}
}
void dumpTGTZ(const char *arg) {
printf("%s\n", arg);
for (int i=0; i < 16; i++) {
printf("%02d: 0x%02x opr 0 -> 0x%02x \n", i, TargetMEM[i], TargetMEM[i+32]);
}
}
int main(void) {
initTGT();
cmeq16V(TargetMEM);
dumpTGT("cmeq");
cmeqZ16V(TargetMEM);
dumpTGTZ("cmeq zero");
cmhs16V(TargetMEM);
dumpTGT("cmhs");
cmge16V(TargetMEM);
dumpTGT("cmge");
cmhi16V(TargetMEM);
dumpTGT("cmhi");
cmgt16V(TargetMEM);
dumpTGT("cmgt");
cmtst16V(TargetMEM);
dumpTGT("cmtst");
cmleZ16V(TargetMEM);
dumpTGTZ("cmle zero");
cmltZ16V(TargetMEM);
dumpTGTZ("cmlt zero");
cmgeZ16V(TargetMEM);
dumpTGTZ("cmge zero");
cmgtZ16V(TargetMEM);
dumpTGTZ("cmgt zero");
return 0;
}
実機実行結果の確認
以下のようにしてビルドして実行しています。
$ gcc -g -O0 simdcmpi.c simdcmpi.s $ ./a.out
結果は比較しやすいように「近縁なもの」同士を並べてみました。
-
- CMEQ、CMEQ(即値ゼロ)、CMTST
CMTSTは、ソース1とソース2のANDをとって0だったらオール0、非0ならオール1です。
-
- CMGE、CMGT、CMHI、CMHS
「大なり」系4種のそろい踏みですが、符号の有り無、イコールを含む含まないで挙動が変化するのが分かるかと思います。
-
- 全て即値ゼロに対するCMGE、CMGT、CMLE、CMLT
対ゼロ系比較は充実。



