今回も「A64の命令多すぎ」感を醸し出す命令であります。浮動小数とSIMD(スカラー扱い)レジスタに対するロード、ストア命令の「一翼を担う」LDUR/STUR命令です。似たアドレシングモードはLDR/STRでも使えるのだけれども、ちょっと違うんだと。Armの八方美人的体質の成せる技?あれば使ってしまうの道理かと。
※「ぐだぐだ低レベルプログラミング」投稿順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
LDUR/STUR命令
LDUR/STURは、符号付即値オフセット9ビット(スケーリング無)を用いたアドレシングのためのロード、ストア命令です。忘却力の年寄は、あれれLDR/STR命令にもそういうアドレシングあったんじゃなかったっけ、と思うのです。しかし、以下のようです。
-
- LDR/STRの符号付即値オフセット9ビット(スケーリング無)はプリインデックスまたはポストインデックス・モードのみ
- LDR/STRの単純なベース+即値オフセットの場合は、符号無即値オフセット12ビット(スケーリング有)
つまり、即値オフセットだけみれば同じ形式はあるのですが、プリ/ポスト・インデックスなのでポインタを更新(破壊)してしまうタイプ。単純な即値オフセットは符合無の上スケーリング有なので、単純にバイト・オフセットを足し込むことはできない、と帯に短し襷に長し状態。そこでわざわざニーモニックを別に設けて「U」一文字(UはunscaledのU)LDUR/STUR命令があるみたいっす。
アセンブラ表記では、常にバイト単位に表記するので、以下のldr命令の#8は、機械語にエンコードされた即値としては符合無12ビットの「2」。
ldr s0, [x0, #8]
一方以下のldur命令の#8は、機械語にエンコードされた即値は符合付9ビットの「8」。
ldur s0, [x0, #8]
そして上下ともx0に格納されたベースアドレスが同じであれば同じアドレスを指すことになります。
どういうときにそんな命令が役に立つの?と勝手に考えてみると、多分、パックドな構造体のアンアライメントなメンバアクセスのときなど都合が良いのでは、くらいしか思いつきません。まあね、ArmはRISCのクセにアンアライメントなアクセスを許しているものね。。。パックドな構造体にアクセス簡単にして~と文句を言った誰かにいい顔みせたかったのではないかと。知らんけど。
今回実験のアセンブリ言語関数
「いつものように手抜きな」関数プロローグ、エピローグ無の被テスト関数群が以下に。引数からポインタをもらって被テスト関数のベースとして使う前に、一律+8してます。マイナスのオフセットの実験用デス。また、ldr命令との比較のために、12bit 符合無スケーリング有の ldr命令を使う関数も入れてあります。
例によって、8ビット幅から128ビット幅まで数々のオペランド・サイズをとることができるのに、32ビット幅の単精度浮動小数のみの大幅手抜きです。
.globl fldurS8, fldurSM8, fldrSImmU, fsturS4, fsturSM4 .text .balign 4 fldrSImmU: add x0, x0, #8 ldr s0, [x0, #8] ret fldurS8: add x0, x0, #8 ldur s0, [x0, #8] ret fldurSM8: add x0, x0, #8 ldur s0, [x0, #-8] ret fsturS4: add x0, x0, #8 ldr s0, dlabel1 stur s0, [x0, #4] ret fsturSM4: add x0, x0, #8 ldr s0, dlabel2 stur s0, [x0, #-4] ret .balign 4 dlabel1: .single 1.2345e3 dlabel2: .single 2.2345e3
C言語記述のmain関数
「通り一遍さわるだけの手抜きな」C言語記述のテスト駆動部です。被テスト関数には引数として単精度配列(の先頭アドレス)を与えてます。内部で+8しているのでベースアドレスは配列要素で[2]をポイントすることになります。バイトオフセット+8なら配列要素的には[4]、オフセットー8なら[0]となります。
#include <stdio.h> #include <stdint.h> #define MAXMEM (10) float TargetMEM[MAXMEM]; extern float fldurS8(float *); extern float fldurSM8(float *); extern float fldrSImmU(float *); extern void fsturS4(float *); extern void fsturSM4(float *); void initTGT() { for (int i=0; i < MAXMEM; i++) { TargetMEM[i] = 1.1f * i; } } int main(void) { float resultS; initTGT(); resultS = fldurS8(TargetMEM); printf ( "fldurS8: %f\n", resultS); resultS = fldurSM8(TargetMEM); printf ( "fldurSM8: %f\n", resultS); resultS = fldrSImmU(TargetMEM); printf ( "fldrSImmU: %f\n", resultS); fsturS4(TargetMEM); printf ("S4: %f\n", TargetMEM[3]); fsturSM4(TargetMEM); printf ("SM4: %f\n", TargetMEM[1]); return 0; }
ビルドして実行
ビルドして実行した結果が以下に。
アセンブラ表記的には、LDUR命令を使ったfldurS8とLDR命令のfldrSImmUはu一文字違いでクリソツ、かつポイントしている配列要素もまったく同じであることが分かります。またfldurSM8により、マイナス方向へもオフセットできているこおも確認できます。S4、SM4のところでは、STURも同様にプラスマイナスのバイトオフセットの足し込みができていることが分かります。
微妙な命令だけれどもパックドな構造体を扱う人には有効?でも使っていても気づいてないか。