ぐだぐだ低レベルプログラミング(151)ARM64(AArach64)SIMD 抽出系

Joseph Halfmoon

前回はSIMD要素のビット幅が倍になる勝手命名「拡張系」の皆さんでした。今回はSIMD要素のビット幅が半分になる勝手命名「抽出系」の皆さんです。逆方向だから前回の逆で4命令かと思えば、倍の8命令もあります。サチュレーションとかいろいろあるのよ抽出系。符号もちょいと捻くれたものもあるし。それにしても命令多過ぎ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

SIMDの「抽出系」

以下の8ニーモニックを「抽出系」に勝手分類いたしました。

    1. XTN、 XTN2
    2. UQXTN、 UQXTN2
    3. SQXTN、 SQXTN2
    4. SQXTUN、 SQXTUN2

まず、半分のビット幅になった結果をSIMDレジスタの下側に詰めるか、上側に詰めるかで末尾に「2」が付くか付かないか(ついたら上側、MSB側ね)の種別があります。そして1のXTNは符号に関わりなく機械的に半分のビット幅にバッサリやるもの、2のUQXTNは元の数値を符号無と解釈し、半分のビット幅に収まらないときにはサチュレーションさせるもの、3のSQXTNは元の数値を符号付と解釈し、半分のビット幅に収まらないときにはサチュレーションさせるものです。まあここまではアリガチ?

しかし、4番目のSQXTUNは、「 Signed saturating extract unsigned narrow 」です。符合付なんだか符号無なんだかハッキリしろい、という感じ。元の倍幅の数値は符号付として解釈し、半分の幅するときは符号無の範囲になるようにサチュレーションさせると。確かに至れり尽くせりの充実の命令セットだけれども端から練習するのが半端ねえす。

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

例によって手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下に。例によってメンドクセーので、ソースはハーフワード幅エレメント、デスティネーションはバイト幅のエレメント、SIMDレジスタは128ビット幅という設定のみで実習をしております。

例によりこの手のビット幅か変化する命令群ではオペランドのビット幅指定がちょっとややこしいデス。

.globl	xtn8V, xtn2_8V, uqxtn8V, uqxtn2_8V, sqxtun8V, sqxtun2_8V, sqxtn8V, sqxtn2_8V  
.text
.balign	4

xtn8V:
    ld1  {v0.8H, v1.8H}, [x0]
    xtn  v0.8B,  v1.8H
    st1  {v0.16B}, [x0]
    ret

xtn2_8V:
    ld1  {v0.8H, v1.8H}, [x0]
    xtn2  v0.16B,  v1.8H
    st1  {v0.16B}, [x0]
    ret

uqxtn8V:
    ld1  {v0.8H, v1.8H}, [x0]
    uqxtn  v0.8B,  v1.8H
    st1  {v0.16B}, [x0]
    ret

uqxtn2_8V:
    ld1  {v0.8H, v1.8H}, [x0]
    uqxtn2  v0.16B,  v1.8H
    st1  {v0.16B}, [x0]
    ret

sqxtn8V:
    ld1  {v0.8H, v1.8H}, [x0]
    sqxtn  v0.8B,  v1.8H
    st1  {v0.16B}, [x0]
    ret

sqxtn2_8V:
    ld1  {v0.8H, v1.8H}, [x0]
    sqxtn2  v0.16B,  v1.8H
    st1  {v0.16B}, [x0]
    ret

sqxtun8V:
    ld1  {v0.8H, v1.8H}, [x0]
    sqxtun  v0.8B,  v1.8H
    st1  {v0.16B}, [x0]
    ret

sqxtun2_8V:
    ld1  {v0.8H, v1.8H}, [x0]
    sqxtun2  v0.16B,  v1.8H
    st1  {v0.16B}, [x0]
    ret
C言語記述のmain関数

上記のアセンブリ言語関数を呼び出すmain関数が以下に。これまたいつもの通りの手抜きです。符合無、符号付、バイト、ハーフワード関係なくC言語レベルでは全てuint16_t引数に対して操作させてます(どうせアセンブラにはCの変数型など関係ねー。)

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

#define MAXMEM	(16)
uint16_t TargetMEM[MAXMEM];

extern void xtn8V(uint16_t *);
extern void xtn2_8V(uint16_t *);
extern void uqxtn8V(uint16_t *);
extern void uqxtn2_8V(uint16_t *);
extern void sqxtn8V(uint16_t *);
extern void sqxtn2_8V(uint16_t *);
extern void sqxtun8V(uint16_t *);
extern void sqxtun2_8V(uint16_t *);

void initTGT2() {
    TargetMEM[0] =0xFF00;
    TargetMEM[1] =0x00FF;
    TargetMEM[2] =0xFF00;
    TargetMEM[3] =0x00FF;
    TargetMEM[4] =0xFF00;
    TargetMEM[5] =0x00FF;
    TargetMEM[6] =0xFF00;
    TargetMEM[7] =0x00FF;
    TargetMEM[8]  =0x007E;
    TargetMEM[9]  =0x007F;
    TargetMEM[10] =0xFF7E;
    TargetMEM[11] =0xFF7F;
    TargetMEM[12] =0xFF80;
    TargetMEM[13] =0xFF81;
    TargetMEM[14] =0x00FE;
    TargetMEM[15] =0xFFFE;
}

void initTGT() {
    TargetMEM[0] =0xFF00;
    TargetMEM[1] =0x00FF;
    TargetMEM[2] =0xFF00;
    TargetMEM[3] =0x00FF;
    TargetMEM[4] =0xFF00;
    TargetMEM[5] =0x00FF;
    TargetMEM[6] =0xFF00;
    TargetMEM[7] =0x00FF;
    TargetMEM[8] =0x0100;
    TargetMEM[9] =0x7F7E;
    TargetMEM[10] =0x8180;
    TargetMEM[11] =0xFFFE;
    TargetMEM[12] =0xFFFD;
    TargetMEM[13] =0x8280;
    TargetMEM[14] =0x7F7D;
    TargetMEM[15] =0x0200;
}

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

int main(void) {
    initTGT();
    xtn8V(TargetMEM);
    dumpTGT("xtn");

    initTGT();
    xtn2_8V(TargetMEM);
    dumpTGT("xtn2");

    initTGT2();
    uqxtn8V(TargetMEM);
    dumpTGT("uqxtn");

    initTGT2();
    uqxtn2_8V(TargetMEM);
    dumpTGT("uqxtn2");

    initTGT2();
    sqxtn8V(TargetMEM);
    dumpTGT("sqxtn");

    initTGT2();
    sqxtn2_8V(TargetMEM);
    dumpTGT("sqxtn2");

    initTGT2();
    sqxtun8V(TargetMEM);
    dumpTGT("sqxtun");

    initTGT2();
    sqxtun2_8V(TargetMEM);
    dumpTGT("sqxtun2");

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

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

$ gcc -g -O0 simdxtn.c simdxtn.s
$ ./a.out

標準出力に「ダラダラ」現れた結果を、命令毎に並べたものが以下に。ポイントとなる部分を色枠で囲ってあります。

    • XTN、XTN2

まずは基本となるXTN命令です。左側のソースのハーフワード(16ビット)のうち下の8ビットを機械的に抽出し、デスティネーションに書き込んでます(赤枠。)その際、ちょこっと非対称なのは下(LSB)側に書き込むXTN命令はSIMDレジスタの上側はゼロクリアするのに対して、上側に書き込むXTN2命令は下側は前の値キープです(青枠。)xtn

 

    • UQXTN、UQXTN2

続くのは、元の値を符号無ハーフワード解釈し、書き込む際にはサチュレーションさせるもの。ハーフワードの値が0x00~0xFF範囲であればそのままですが、ひとたび0x100以上の値であると0xFFにサチュレーションさせます。以下の黄色枠がサチュレーションしているところね。上側バイトが0xFFばかりで多様性が失われてますけど。uqxtn

 

    • SQXTN、SQXTN2

続くのは符号付きです。説明のため、着目点に黄色枠をつけました。ソースが0x00feの場合、符号付解釈するとプラスです。バイト幅の数でプラスの最大値は0x7fであるので、0x00feはこの命令によりサチュレートされて0x7Fとなります。一方、元の値が0xfffeの場合、マイナスの数です。バイト幅にしても0xfeという表現で行けるので転送先でも0xfeのままとなります。sqxtn

 

    • SQXTUN、SQXTUN2

続くのはデスティネーション解釈は符号付き、結果は符号無範囲にサチュレートです。説明のため、着目点に黄色枠をつけました。ソースが0x00feの場合、符号付解釈するとプラスです。符号無バイト幅でも0xfeで表現できるのでそのままです。一方、元の値が0xfffeの場合はマイナスの数です。行先は符号無なので負の数はなんでも0にサチュレーションしてしまいます。sqxtun

いつもの通り、命令多過ぎA64。でもあれば便利だから使うね。

ぐだぐだ低レベルプログラミング(150)ARM64(AArach64)SIMD 拡張系 へ戻る

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