ぐだぐだ低レベルプログラミング(142)ARM64(AArach64)SIMD ビット幅変?

Joseph Halfmoon

前回前々回とA64のSIMD比較命令を練習。今回から要素のビット幅が「変わる」SIMD算術演算命令に入ります。通常のSIMD命令は要素のビット幅は不変なのでコイツ等はちょっと変わり者です。しかし変わり者といえどフツーにひと揃いの演算が含まれております。命令多過ぎA64。いったい何個あるんじゃあ。

※「ぐだぐだ低レベルプログラミング」投稿順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

要素のビット幅が狭くなる、広くなる命令群

演算すると要素のビット幅が狭くなる(narrow)と広くなる(wide)命令セットには、加減算、乗算、積和演算など一通りの命令が備わっています。今回は、まず「演算パターン」を抑えておくべし、とのことで加算系命令に絞って練習することにいたしました。しかし、加算に限っても以下の12ニーモニック分も命令があるのです。

    • ADDHN, ADDHN2 Add returning high, narrow
    • RADDHN, RADDHN2 Rounding Add returning high, narrow
    • SADDL, SADDL2 Signed Add Long
    • SADDW, SADDW2 Signed Add Wide
    • UADDL, UADDL2 Unsigned Add Long
    • UADDW, UADDW2 Unsigned Add Wide

狭くなる奴が丸め付と丸め無で2種類、しかし2がつく奴と2がつかない奴がいるので2x2で合計4種類です。広くなるものが符合付、符号無で2種類、上と同様に2がつく奴と2がつかない奴がある上に、LongとWideという区別まであります。2x2x2で8種類ね。総合計12種類デス。やってられんな。。。

今回は、以下の4ニーモニックに絞って練習してみることにいたしました。

    1. ADDHN
    2. ADDHN2
    3. UADDW
    4. UADDW2

こ奴らのパターンを抑えておけば、なんとかなるっと。ホントか?

ADDHNとADDHN2を図示してみる

英語の文章でくだくだ説明されても忘却力の年寄は読んだそばから忘れてしまうので図にしてみましたぞ。16ビット幅の整数型要素2つを足して「ハイ側」のバイトを8ビット幅の「狭い」整数型のベクトルとして取り出す操作です。こんな感じ。ADDHN_FIG

図にしたら何てこともありません。2がついているのとついていないのは結果の8ビット要素のベクトルをデスティネーション・レジスタの上側に置くか、下側に置くかの違いっす。

しかし、アセンブラのコーディング上は、8Bだったり8Hだったり16Bだったりと表記が混在しとります。慣れないと大混乱?慣れたりしないか、こんな命令。。。

UADDWとUADDW2を図示してみる

今度は要素のビット幅を「広く」する命令です。こちらの場合、一方のソースの幅が「広く」て、他方が「狭い」形です。そして計算すると広い方に「合わせた結果」が得られます。第1ソースが「広い」か、第2ソースが「広い」かでWIDEとLONGの種別あり、「じゃない」方が「狭い」となります。メンドクセーので図示してみます。こんな感じ。UADDW_FIG

上記は16ビット幅要素に8ビット幅要素を足して結果16ビットになるパターンンです。2がついているのとついていないのは幅が狭い側のソースをレジスタの上側に置くか、下側に置くかの違いです。

ここでも8Hと書くか、8Bと書くか、16Bと書くか混乱します。アセンブラが文句を言ったら考えるっと。

実験に使ったアセンブリ言語記述の被テスト関数

例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下です。

.globl	addhn8V, addhn28V, uaddw8V, uaddw28V 
.text
.balign	4

addhn8V:
    ld1  {v1.8H, v2.8H}, [x0], #32
    addhn  v0.8B,  v1.8H, v2.8H
    st1  {v0.8H}, [x0]
    ret

addhn28V:
    ld1  {v1.8H, v2.8H}, [x0], #32
    addhn2  v0.16B,  v1.8H, v2.8H
    st1  {v0.8H}, [x0]
    ret

uaddw8V:
    ld1  {v1.8H, v2.8H}, [x0], #32
    uaddw  v0.8H,  v1.8H, v2.8B
    st1  {v0.8H}, [x0]
    ret

uaddw28V:
    ld1  {v1.8H, v2.8H}, [x0], #32
    uaddw2  v0.8H,  v1.8H, v2.16B
    st1  {v0.8H}, [x0]
    ret
C言語記述のmain関数

上記のアセンブリ言語関数を呼び出すmain関数が以下に。Cで書くと今回も何気にメンドイのは、アセンブラへ行って帰ってくると「実際の」変数型が変化してしまうことです。Cのレベルでは全てuint16_t型じゃと割り切って、テキトーに出し入れしてます。

#include <stdio.h>
#include <stdint.h>

#define MAXMEM	(24)

uint16_t TargetMEM[MAXMEM];

extern void addhn8V(uint16_t *);
extern void addhn28V(uint16_t *);
extern void uaddw8V(uint16_t *);
extern void uaddw28V(uint16_t *);

void initTGT1() {
    TargetMEM[0]  = 0x0000;
    TargetMEM[1]  = 0x0000;
    TargetMEM[2]  = 0x0001;
    TargetMEM[3]  = 0x0001;
    TargetMEM[4]  = 0x0080;
    TargetMEM[5]  = 0x0080;
    TargetMEM[6]  = 0x0100;
    TargetMEM[7]  = 0x0100;
    TargetMEM[8]  = 0x0000;
    TargetMEM[9]  = 0x00FF;
    TargetMEM[10] = 0x0000;
    TargetMEM[11] = 0x00FF;
    TargetMEM[12] = 0x0000;
    TargetMEM[13] = 0x00FF;
    TargetMEM[14] = 0x0000;
    TargetMEM[15] = 0x01FF;
    TargetMEM[16] = 0x0000;
    TargetMEM[17] = 0x0000;
    TargetMEM[18] = 0x0000;
    TargetMEM[19] = 0x0000;
    TargetMEM[20] = 0x0000;
    TargetMEM[21] = 0x0000;
    TargetMEM[22] = 0x0000;
    TargetMEM[23] = 0x0000;
}

void initTGT2() {
    TargetMEM[0]  = 0x0100;
    TargetMEM[1]  = 0x0100;
    TargetMEM[2]  = 0x0200;
    TargetMEM[3]  = 0x0200;
    TargetMEM[4]  = 0x0300;
    TargetMEM[5]  = 0x0300;
    TargetMEM[6]  = 0x0400;
    TargetMEM[7]  = 0x0400;
    TargetMEM[8]  = 0x0100;
    TargetMEM[9]  = 0x0302;
    TargetMEM[10] = 0x0504;
    TargetMEM[11] = 0x0706;
    TargetMEM[12] = 0x0708;
    TargetMEM[13] = 0x0708;
    TargetMEM[14] = 0x0708;
    TargetMEM[15] = 0x0708;
    TargetMEM[16] = 0x0000;
    TargetMEM[17] = 0x0000;
    TargetMEM[18] = 0x0000;
    TargetMEM[19] = 0x0000;
    TargetMEM[20] = 0x0000;
    TargetMEM[21] = 0x0000;
    TargetMEM[22] = 0x0000;
    TargetMEM[23] = 0x0000;
}

uint16_t getLow(int idx, int pos) {
    if (idx & 0x01) {
        return (TargetMEM[(idx>>1)+pos] >> 8) & 0xFF; 
    } else {
        return TargetMEM[(idx>>1)+pos] & 0xFF;
    }
}

uint16_t getHigh(int idx, int pos) {
    if (idx & 0x01) {
        return (TargetMEM[(idx>>1)+pos] >> 8) & 0xFF; 
    } else {
        return TargetMEM[(idx>>1)+pos] & 0xFF;
    }
}

void dumpTGTL(const char *arg) {
    printf("%s\n", arg);
    for (int i=0; i < 8; i++) {
        printf("%02d: 0x%04x opr 0x%04x -> 0x%02x\n", i, TargetMEM[i], TargetMEM[i+8], getLow(i, 16));
    }
}

void dumpTGTH(const char *arg) {
    printf("%s\n", arg);
    for (int i=0; i < 8; i++) {
        printf("%02d: 0x%04x opr 0x%04x -> 0x%02x\n", i, TargetMEM[i], TargetMEM[i+8], getHigh(i, 20));
    }
}

void dumpTGTL2(const char *arg) {
    printf("%s\n", arg);
    for (int i=0; i < 8; i++) {
        printf("%02d: 0x%04x opr 0x%02x -> 0x%04x\n", i, TargetMEM[i], getLow(i, 8), TargetMEM[i+16]);
    }
}

void dumpTGTH2(const char *arg) {
    printf("%s\n", arg);
    for (int i=0; i < 8; i++) {
        printf("%02d: 0x%04x opr 0x%02x -> 0x%04x\n", i, TargetMEM[i], getHigh(i, 12), TargetMEM[i+16]);
    }
}

int main(void) {
    initTGT1();

    addhn8V(TargetMEM);
    dumpTGTL("addhn");

    addhn28V(TargetMEM);
    dumpTGTH("addhn2");

    initTGT2();
    uaddw8V(TargetMEM);
    dumpTGTL2("uaddw");

    uaddw28V(TargetMEM);
    dumpTGTH2("uaddw2");

    return 0;
}
実機実行結果の確認

以下のようにしてビルドして実行しています。

$ gcc -g -O0 simdaddhn.c simdaddhn.s
$ ./a.out
    • ADDHN、ADDHN2

addhn

 

 

 

 

    • UADDW、UADDW2

uaddw

計算は出来たみたい。メンドクセーけど。

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

ぐだぐだ低レベルプログラミング(143)ARM64(AArach64)SIMD ビット幅変2 へ進む