前回ようやく2要素を演算した結果の要素のビット幅が変わる奴らをやっつけ終えました。今回から1要素を処理して1要素が得られる系統の命令に入ります。ともかく命令数が多いのでコマケー話は踏みつぶしてサッサと通り過ぎたい気でいるのですがどうなることか。その初回は「カウント系」です。クセさえ分かればどおってことない。ホントか?
※「ぐだぐだ低レベルプログラミング」投稿順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
ソース1要素を処理してデスティネーション1要素に書き込む命令
この系統の代表選手は、平方根を求める
FSQRT
ではないかと思います。ソースの浮動小数1要素の平方根を求めてデスティネーションに書き込むもの。「ひとよひとよにひとみごろ」っとくらあ。まあSIMD演算なので要素の個数だけ並列に求めるわけでありますが。しかし、FSQRTのようなアリガチな命令ばかりではありません。今回とりあげますのはカウント系(ビット・カウント)の命令3種です。いずれも整数型の要素に対して要素内の「ビット数」を数えるもの。列挙すれば以下のとおり。
-
- CLS、 Count leading sign bits
- CLZ、 Count leading zero bits
- CNT、 Population count per byte
「1」のCLSってのは何かい。符合ビットを数えるのだね。負の数なら先行する1のビット数を数えればよいな。正の数なら先行する0のビット数か。あれあれ「2」のCLZも先行するゼロのビット数じゃん!
違いは何かと言えば、例えば1バイト0x00をCLSとCLZで数えたときに分かります。CLSで数えると符号ビット(0)が7個、最後のビットは符合ビットではなく数本体ということになるみたいです。結果は7ね。一方CLZだとともかく先行するゼロの個数を数えるので結果は8。数え方が違うのよ。
その点「3」のCNT命令は1が立っているビットの数を数えるだけと明快。0x00相手なら0だ。
実験に使ったアセンブリ言語記述の被テスト関数
いつものように手抜きの関数プロローグ、エピローグ無の被テスト関数のソースが以下です。CNT命令はバイト幅のみですが、CLSとCLZはハーフワード、ワードもオペランドにとれます。ただしここではメンドイのでバイトのみです。またSIMDレジスタは64ビット幅で使ってます。手抜きだよう。
.globl cls8V, clz8V, cnt8V .text .balign 4 cls8V: ld1 {v1.8B}, [x0], #8 cls v0.8B, v1.8B st1 {v0.8H}, [x0] ret clz8V: ld1 {v1.8B}, [x0], #8 clz v0.8B, v1.8B st1 {v0.8H}, [x0] ret cnt8V: ld1 {v1.8B}, [x0], #8 cnt v0.8B, v1.8B st1 {v0.8H}, [x0] ret
C言語記述のmain関数
上記のアセンブリ言語関数を呼び出すmain関数が以下に。符号付き数でもCのレベルでは全てuint8_t型で書いているもの。
#include <stdio.h> #include <stdint.h> #define MAXMEM (16) uint8_t TargetMEM[MAXMEM]; extern void cls8V(uint8_t *); extern void clz8V(uint8_t *); extern void cnt8V(uint8_t *); void initTGT() { TargetMEM[0] = 0x00; TargetMEM[1] = 0x01; TargetMEM[2] = 0x02; TargetMEM[3] = 0x03; TargetMEM[4] = 0xFF; TargetMEM[5] = 0xFE; TargetMEM[6] = 0xFD; TargetMEM[7] = 0xFC; 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 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]); } } int main(void) { initTGT(); cls8V(TargetMEM); dumpTGT("cls"); initTGT(); clz8V(TargetMEM); dumpTGT("clz"); initTGT(); cnt8V(TargetMEM); dumpTGT("cnt"); return 0; }
実機実行結果の確認
以下のようにしてビルドして実行しています。
$ gcc -g -O0 simdcnt.c simdcnt.s $ ./a.out
よかった、3種類のビットカウントをしてるみたい。実行してみれば分かり易い奴らだよ。