前回はSIMDのビットカウント系命令を練習しました。今回はSIMDの符号操作系命令です。地味な命令が続くな~。今回対象は6命令です。整数と浮動小数、絶対値とネゲート、その組み合わせだけだったら4命令じゃん。残りの2つは何?何を隠そう整数型には「サインド・サチューレーティング」があるのよ、なんじゃらほい。
※「ぐだぐだ低レベルプログラミング」投稿順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符号操作系命令
勝手に「符合操作系」に分類した操作は絶対値と符号反転(ネゲート)の2つです。絶対値は負の値のときだけ同じ「絶対値」の正の値を得るもの、正の値のときは何もしませぬ。一方符号反転は、正であれば負、負であれば正の値を得るものです。この2種の操作を整数および浮動小数に施すための命令が用意されています。ただし整数に対してはそれぞれ命令が2種類に分かれます。これは2の補数表現の整数についてはプラスとマイナスで表現できる範囲が対称でないためだと思われます。負の絶対値最大は正の値に変換できないので、その時サチュレーションを行う命令も存在します。合計6種命令を列挙すると以下のとおり。
-
- ABS Absolute value
- FABS Floating-point absolute
- SQABS Signed saturating absolute value
- NEG Negate
- FNEG Floating-point negate
- SQNEG Signed saturating negate
実験に使ったアセンブリ言語記述の被テスト関数
いつものように手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下です。整数に対する命令はバイト、ハーフワード、ワード、ダブルワード幅に対して処理可能ですが、手抜きでバイトだけです。それもSIMDレジスタ幅は半分の64ビットのみ使用。一方浮動小数に対する命令は単精度浮動小数だけです。でもSIMDレジスタ幅はフルの128ビット幅。いつもの手抜きね。
.globl abs8V, neg8V, sqabs8V, sqneg8V, fabs4V, fneg4V .text .balign 4 abs8V: ld1 {v1.8B}, [x0], #8 abs v0.8B, v1.8B st1 {v0.8H}, [x0] ret neg8V: ld1 {v1.8B}, [x0], #8 neg v0.8B, v1.8B st1 {v0.8H}, [x0] ret sqabs8V: ld1 {v1.8B}, [x0], #8 sqabs v0.8B, v1.8B st1 {v0.8H}, [x0] ret sqneg8V: ld1 {v1.8B}, [x0], #8 sqneg v0.8B, v1.8B st1 {v0.8H}, [x0] ret fabs4V: ld1 {v1.4S}, [x0], #16 fabs v0.4S, v1.4S st1 {v0.4S}, [x0] ret fneg4V: ld1 {v1.4S}, [x0], #16 fneg v0.4S, v1.4S st1 {v0.4S}, [x0] ret
C言語記述のmain関数
上記のアセンブリ言語関数を呼び出すmain関数が以下に。符号付き整数に対する処理でもCのレベルでは全てuint8_t型で書いているもの。
#include <stdio.h> #include <stdint.h> #define MAXMEM (16) #define MAXMEM2 (8) uint8_t TargetMEM[MAXMEM]; float TargetMEM2[MAXMEM2]; extern void abs8V(uint8_t *); extern void neg8V(uint8_t *); extern void sqabs8V(uint8_t *); extern void sqneg8V(uint8_t *); extern void fabs4V(float *); extern void fneg4V(float *); void initTGT() { TargetMEM[0] = 0x00; TargetMEM[1] = 0x01; TargetMEM[2] = 0x02; TargetMEM[3] = 0x7F; TargetMEM[4] = 0xFF; TargetMEM[5] = 0xFE; TargetMEM[6] = 0xFD; TargetMEM[7] = 0x80; TargetMEM[8] = 0x00; TargetMEM[9] = 0x00; TargetMEM[10] = 0x00; TargetMEM[11] = 0x00; TargetMEM[12] = 0x00; TargetMEM[13] = 0x00; TargetMEM[14] = 0x00; TargetMEM[15] = 0x00; } void initTGT2() { TargetMEM2[0] = 1.125f; TargetMEM2[1] = 2.5e12f; TargetMEM2[2] = -1.333f; TargetMEM2[3] = -2.5e-2f; TargetMEM2[4] = 0.0f; TargetMEM2[5] = 0.0f; TargetMEM2[6] = 0.0f; TargetMEM2[7] = 0.0f; } void dumpTGT(const char *arg) { printf("%s\n", arg); for (int i=0; i < 8; i++) { printf("%02d: 0x%02x -(%s)-> 0x%02x\n", i, TargetMEM[i], arg, TargetMEM[i+8]); } } void dumpTGT2(const char *arg) { printf("%s\n", arg); for (int i=0; i < 4; i++) { printf("%02d: 0x%f -(%s)-> 0x%f\n", i, TargetMEM2[i], arg, TargetMEM2[i+4]); } } int main(void) { initTGT(); abs8V(TargetMEM); dumpTGT("abs"); initTGT(); neg8V(TargetMEM); dumpTGT("neg"); initTGT(); sqabs8V(TargetMEM); dumpTGT("sqabs"); initTGT(); sqneg8V(TargetMEM); dumpTGT("sqneg"); initTGT2(); fabs4V(TargetMEM2); dumpTGT2("fabs"); initTGT2(); fneg4V(TargetMEM2); dumpTGT2("fneg"); return 0; }
実機実行結果の確認
以下のようにしてビルドして実行しています。
$ gcc -g -O0 simdabs.c simdabs.s $ ./a.out
緑の枠で囲ってあるのが、上のabsと下のnegの違いが見える場所です。ここは縦方向に比較してくだされ。一方赤の枠で囲ってあるのは、右のサインド・サチューレーティング処理付の命令とそうでない普通の命令の処理結果の違いが見えるところです。ここは左右で比較です。
浮動小数は素直だわな。