今回扱うLDP命令には、デスティネーションが2つあります。といって驚いちゃいけません。32ビットのArm命令(A32)には任意本数のレジスタをスポポンと1命令で出し入れするLDM/STMなどという命令まであったことに比べれば、A64は「抑え気味」です。でも言いたくなります。それでもRISCかよ?と。
※「ぐだぐだ低レベルプログラミング」投稿順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です。
ロード「ペア」命令とな
メモリ上連続したレジスタ2本分のメモリ領域を読み取ってどれでもよい汎用レジスタ2本にロードすることができる命令です。オペランドのサイズと、符合拡張、そして non-temporal hint の有無によって以下の3つのニーモニックの命令が存在します。
-
- LDP
- LDPSW
- LDNP
1のLDP命令は、32ビットのWレジスタ2本または64ビットのXレジスタ2本にメモリからロードする命令です。符合無としてロードされます(注1。)
2のLDP命令は、64ビットのXレジスタ2本に32ビット幅の符合付データをロードする命令です。メモリ上は32ビット幅x2個ですが、レジスタ上では64ビットに符合拡張されます。
3のLDNP命令は、機能的には実質1のLDPと同じです。ただし、non-temporal hint 付という点が異なります。ぶっちゃけ、このヒントは「時間的に近いところで再びこのメモリを使うことはないから、キャッシュとかしてくれなくてもよいよー」とハードウエアにお知らせするためのものだと思います。まあ、画像データとか1度読んで処理すればそれでおしまいなものにつけるとよいのかなあ。
3のLDNPには符合拡張が見当たらないとか非対称な部分があります。また、今回の命令は3つとも2本のレジスタに1命令で書き込む荒業な命令なので、オペランドの指定によっては「予想もつかない」挙動をするので注意してね、な命令でもあります。まあ「予想もつかない」といいながら、マニュアルを読めばこういうオペランドだとこうなる、別なオペランドではああなる、みたいなことは列挙されてます。
(注1)当方、以下のちょいと古い版のArm v8のマニュアルを愛用させていただいております。
Arm Architecture Reference Manual ARM DDI 0487E.a (ID070919)
上記マニュアルのLDP命令の動作記述を読むと、その中にLDPSW命令に相当する符合拡張動作の部分まで書かれてちゃってます。ただLDP命令のエンコードでそこをONにする記述方法は書かれてない(というかそこをエンコードするとLDPSW命令になってしまう)ので結局LDPSWと綴って符合拡張にするしかないようです。コマケー話なんだが。
なお、A64の最新のマニュアルは以下でダウンロード可能です。
Arm Architecture Reference Manual for A-profile architecture
毎月レビジョンヒストリが更新されているみたいです。さすがArm、マメだな。手元の版でも7000ページもあるので読むのは辛い(いまだに全部読んでないけど。それでもx86_x64よりは大分薄い)マニュアルです。
実験に使ったアセンブリ言語ソース
例によって手抜きな(関数プロローグもエピローグもない)1命令1関数スタイルです。今回はレジスタ2つにロードするので、1のついている命令で第1のデスティネーション、2のついている命令で第2のディスティネーションを返してます。
.globl ldpW1, ldpW2, ldpsW1, ldpsW2, ldpX1, ldpX2, ldnpW1, ldnpW2, ldnpX1, ldnpX2 .text .balign 4 ldpW1: ldp w0, w1, [x2, #4] ret ldpW2: ldp w0, w1, [x2, #4] mov w0, w1 ret ldpsW1: ldpsw x0, x1, [x2, #4] ret ldpsW2: ldpsw x0, x1, [x2, #4] mov x0, x1 ret ldpX1: ldp x0, x1, [x2, #8] ret ldpX2: ldp x0, x1, [x2, #8] mov x0, x1 ret ldnpW1: ldnp w0, w1, [x2, #4] ret ldnpW2: ldnp w0, w1, [x2, #4] mov w0, w1 ret ldnpX1: ldnp x0, x1, [x2, #8] ret ldnpX2: ldnp x0, x1, [x2, #8] mov x0, x1 ret
実験に使用したC言語ソース
上記のアセンブリ言語関数を呼び出すテスト用のCソースが以下に。
#include <stdio.h> #include <stdint.h> uint32_t tgtW[4]; uint64_t tgtX[4]; extern uint32_t ldpW1(uint32_t, uint32_t, uint32_t *); extern uint32_t ldpW2(uint32_t, uint32_t, uint32_t *); extern uint64_t ldpsW1(uint64_t, uint64_t, uint32_t *); extern uint64_t ldpsW2(uint64_t, uint64_t, uint32_t *); extern uint64_t ldpX1(uint64_t, uint64_t, uint64_t *); extern uint64_t ldpX2(uint64_t, uint64_t, uint64_t *); extern uint32_t ldnpW1(uint32_t, uint32_t, uint32_t *); extern uint32_t ldnpW2(uint32_t, uint32_t, uint32_t *); extern uint64_t ldnpX1(uint64_t, uint64_t, uint64_t *); extern uint64_t ldnpX2(uint64_t, uint64_t, uint64_t *); void initTGT() { tgtW[0] = 0x00234567; tgtW[1] = 0x11234567; tgtW[2] = 0x82234567; tgtW[3] = 0x93234567; tgtX[0] = 0x00A5A5A511112222; tgtX[1] = 0x11A5A5A511112222; tgtX[2] = 0x82A5A5A511112222; tgtX[3] = 0x93A5A5A511112222; } int main(void) { uint32_t uresult; uint64_t uresultX; initTGT(); uresult =ldpW1(0, 0, tgtW); printf ("ldpW1 : 0x%08x\n", uresult); uresult =ldpW2(0, 0, tgtW); printf ("ldpW2 : 0x%08x\n", uresult); uresultX =ldpsW1(0, 0, tgtW); printf ("ldpsW1 : 0x%016lx\n", uresultX); uresultX =ldpsW2(0, 0, tgtW); printf ("ldpsW2 : 0x%016lx\n", uresultX); uresultX =ldpX1(0, 0, tgtX); printf ("ldpX1 : 0x%016lx\n", uresultX); uresultX =ldpX2(0, 0, tgtX); printf ("ldpX2 : 0x%016lx\n", uresultX); uresult =ldnpW1(0, 0, tgtW); printf ("ldnpW1 : 0x%08x\n", uresult); uresult =ldnpW2(0, 0, tgtW); printf ("ldnpW2 : 0x%08x\n", uresult); uresultX =ldnpX1(0, 0, tgtX); printf ("ldnpX1 : 0x%016lx\n", uresultX); uresultX =ldnpX2(0, 0, tgtX); printf ("ldnpX2 : 0x%016lx\n", uresultX); return 0; }
ビルドして実行
ビルドして実行している様子が以下です。まあ、連続したメモリを読み取っているだけ。
昔に比べたら動作は抑え気味だけれど、それでもRISCかよ的な命令。