ぐだぐだ低レベルプログラミング(140)ARM64(AArach64)SIMD cmeq

Joseph Halfmoon

今回練習するのは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

cmeq_cmeqz_cmtst

CMTSTは、ソース1とソース2のANDをとって0だったらオール0、非0ならオール1です。

    • CMGE、CMGT、CMHI、CMHS

cmge_cmgt_cmhi_cmhs

「大なり」系4種のそろい踏みですが、符号の有り無、イコールを含む含まないで挙動が変化するのが分かるかと思います。

    • 全て即値ゼロに対するCMGE、CMGT、CMLE、CMLT

cmgez_cmgtz_cmlez_cmltz

 

対ゼロ系比較は充実。

ぐだぐだ低レベルプログラミング(139)ARM64(AArach64)SIMDsqdmulh へ戻る

ぐだぐだ低レベルプログラミング(141)ARM64(AArach64)SIMD fcmeq へ進む