浮動小数点数ロード/ストア命令()

浮動小数点数の演算は、基本的にメモリに格納されている数値をレジスタに読み込んで、演算し、メモリに書き戻すという動きになります。 ARM64 (ARMv8) では浮動小数点数を1つずつ扱うスカラー型と、複数の浮動小数点数をまとめて扱うベクトル型のレジスタを扱う命令が別に存在します。 今回はスカラー型のレジスタとメモリ間でコピーするロード命令とストア命令を説明します。 整数型のロード/ストア命令と同じ部分が多く、使い易い命令となっています。

ロード命令はメモリに格納されている浮動小数点形式の数値を浮動小数点レジスタに読み込む命令です。 整数用のロード命令と同じ「LDR」というニーモニックを使用します。

ストア命令はレジスタに格納されている浮動小数点の数値をメモリに書き込みます。ロード命令の逆の動作をします。整数用のストア命令と同じ「STR」というニーモニックを使用します。

スカラー型浮動小数点レジスタの名称

整数型の汎用レジスタは 64ビットのレジスタ(Xn) と 32ビットのレジスタ(Wn) の2種類で同じ領域を共有します。 同じように、浮動小数点レジスタは128ビット全体を使う Qn レジスタ、倍精度浮動小数点数を格納する Dn レジスタ、単精度浮動小数点数を格納する Sn レジスタ、半精度浮動小数点数を格納する Hn レジスタ、バイトデータを格納する Bn レジスタが同じ領域を共有します。ここで n はレジスタの番号で 0 から 31 の値です。サイズの小さいデータはレジスタの下位の領域を占有します。Q3 レジスタと D3 レジスタには別の値を格納できません。Vn レジスタという名称を使う場合も同じ領域を使います。 例えば V3.2D[0] というレジスタ は D3 レジスタと同じ領域を使います。

レジスタ名ビット
Bn8
Hn16
Sn32
Dn64
Qn128

浮動小数点数のロード/ストア命令では転送先のレジスタのサイズとして 8、16、32、64、128ビットを指定することができます。 整数型のロード命令では転送先のレジスタのサイズに 32 ビットと、64ビットの2種類です。 そのためメモリ中のビット数 (8、16、32、64) とレジスタのビット数を一致させるため、命令の数が多くなっていますが、浮動小数点数のロード/ストア命令では転送元のメモリ中のデータサイズと転送先のレジスタのサイズが一致するため、命令が単純です。64ビットの浮動小数点数のロード/ストア命令は、アドレッシングも含めて以下の12種類だけです。サイズが異なる場合でも Dt (t は0から31) を Bt、Ht、St、Qt に変更するだけです。

  LDR  Dt, [base], #simm9     // ポストインデックス
  STR  Dt, [base], #simm9     // ポストインデックス

  LDR  Dt, [base, #simm9]!    // プレインデックス
  STR  Dt, [base, #simm9]!    // プレインデックス

  LDUR Dt, [base {,#simm9}]   // 符号付きオフセット (+255..-256)
  STUR Dt, [base {,#simm9}]   // 符号付きオフセット (+255..-256)

  LDR  Dt, [base {,#uimm12}]   // 符号なしオフセット [0 - 4095]
  STR  Dt, [base {,#uimm12}]   // 符号なしオフセット [0 - 4095]

  LDR  Dt, [base, Wm {,extend {#0}}] // レジスタオフセット
  STR  Dt, [base, Wm {,extend {#0}}] // レジスタオフセット

  LDR  Dt, [base, Xm {,extend {#0}}] // レジスタオフセット
  STR  Dt, [base, Xm {,extend {#0}}] // レジスタオフセット

アドレッシング

浮動小数点型のロード/ストア命令でも読み書きするメモリの位置(メモリアドレス)を 指定する必要があります。 浮動小数点数でも整数用のロード命令と同じアドレッシングを使用しますが、 イミディエートオフセットレジスターオフセットの2種類だけです。

ロード命令では、ベースレジスタとして働く汎用レジスタ (Xn) または スタックポインタ (SP) の示すメモリの番地を基本として、オフセットと呼ばれる数値を加減算したメモリアドレスのデータを読み込みます。

fstr

ストア命令も同じく、ベースレジスタの示すメモリの番地に対してオフセットを加減算したメモリアドレスにデータを書き込みます。

fstr

図の中の「オフセット」が定数のものがイミディエートオフセット、 このオフセットもレジスタに格納された値を使うものが レジスターオフセットです。

例えば

.include        "stdio.s"
.include        "float2string.s"

.global _start
.text

_start:
        adr     x1, LABEL
        ldr     d0, [x1]
        bl      Double2String
        bl      OutAsciiZ
        bl      NewLine
        bl      Exit

LABEL:   .double  1.2345678987654321
$ as -o fldr.o fldr.s
$ ld -o fldr fldr.o
$ ./fldr
 1.234567898765432

イミディエートオフセット

レジスタが格納している値をメモリアドレスとして、その値に定数を 加えてメモリアドレスとするアドレッシングをイミディエートオフセット (immediate offset) といいます。ARM64では、64ビットレジスタに メモリ中のアドレスを格納して、そのメモリアドレスに対してデータを 読み書きします。 レジスタが格納しているメモリアドレスに対して、 比較的小さい整数値、例えば +16 とか -200 といった定数値を加えた アドレスにアクセスしたい場合があります。 そのような場合のために 命令自体に比較的小さい整数値を埋め込むことができます。 この値をイミディエートと呼び、日本語では即値と訳されます。

符号付き 9 ビットオフセット

ロード命令では、 9ビットの符号付き定数(+255..-256)を使う アドレッシングとして、プレインデックスとポストインデックス があります。これらの 2 種類はベースレジスタにオフセットを 加えてベースレジスタを更新します。 また、LDUR 系の命令として ベースレジスタへの変更を行わない符号付き 9 ビットオフセット のアドレッシングがあります。

プレインデックス

プレインデックスは、ベースレジスタと呼ぶスタックポインタ (SP) または 64 ビット汎用レジスタ (Xn) が格納しているアドレスに 定数を加えたメモリアドレスから読み出し、そのアドレスをベースレジスタに書き戻します。 メモリにアクセスする前に ベースレジスタの値を更新します。スタックにレジスタを退避 (PUSH) するような場合に使います。 スタックポインタの値を減算して、そこに退避するレジスタ(ここではD0)の内容を 書き込むという動作は「str d0, [sp, #-16]!」で実現できます。

次の擬似コードでは、memory[ adr ] でメモリアドレス adr に格納されている内容を示します。 base はベースレジスタ(Xn または SP)、Xt は転送先のレジスタを示します。 プレインデックスはオペランドの最後に「!」を付けて表します。

  LDR   Dt, [base,  #simm9]!     // プレインデックス

    base = base + simm9;
    Dt = memory[base];

  STR   Dt, [base,  #simm9]!     // プレインデックス

    base = base + simm9;
    memory[base] = Dt;
ポストインデックス

ポストインデックスはベースレジスタの示すメモリアドレスから 読みだした後、オフセットに指定した定数をベースレジスタに加算します。 つまり、ベースレジスタのメモリアドレスにアクセスした「後に」 ベースレジスタを別のメモリアドレスに更新することから「ポストインデックス」と呼んでいます。

  LDR   Dt, [base], #simm9       // ポストインデックス

    Dt = memory[base];
    base = base + simm9;

  STR   Dt, [base], #simm9       // ポストインデックス

    memory[base] = Dt;
    base = base + simm9;
Unscaled

9ビットの符号付き定数を使うアドレッシングモードにはもう一つあって、LDUR/STUR 命令は、ポストインデックスでもプレインデックスでもない動作をします。ベースレジスタの値は変化しません。

  LDUR  Dt, [base, #simm9]       // 書き戻しなし

    Dt = memory[base + simm9];

  STUR  Dt, [base, #simm9]       // 書き戻しなし

    memory[base + simm9] = Dt;

この LDUR/STUR 命令も「LDR/STR」というニーモニック(命令の名前)のほうが 適していると思いますが、ARM (会社の方) は次に説明する符号無し 12 ビットオフセットアドレッシングの方に「LDR/STR」というニーモニックを 割り当てることにしたようです。「U」は Unscaled の略で、 次の符号無し12ビットオフセットと異なって、 オフセットに対して転送するバイト数を乗算するスケーリングを 行わないことを意味しています。 いつも LDR というニーモニックを 使っていれば、必要な場合に LDUR 命令と LDR 命令の符号無し 12 ビット オフセットをアセンブラが自動的に選択して使ってくれるので、 通常はこのアドレッシングモードを意識する必要はありません。 転送バイト数の倍数になっていないオフセット値で、かつオフセットが +255 .. -256 を超える場合に、アセンブラはエラーを出力します。

符号無し12ビットオフセット

ベースレジスタに格納されている値に符号無し 12 ビットの即値 (0 .. 4095) を加えた値のメモリアドレスに格納されているメモリの内容を レジスタにコピーします。

  LDR  Dt, [base, #uimm12]       // 書き戻しなし

    Dt = memory[base + uimm12 * scale];  // scale = 1,2,4,8

12ビットの定数は転送するバイト数にしたがって 1、2、4、8、16 倍に スケーリングされます。したがって実際のオフセットは1バイトのロードでは (0 .. 4095)、16ビットでは (0 .. 8190)、32ビットでは (0 .. 16380)、64ビットでは (0 .. 32760)、128ビットでは (0 .. 65520) の 範囲のオフセットを使用することができます。C言語の配列で考えると 4096 個の 要素に定数インデックスでアクセスできることになります。 符号付き 9 ビットオフセットと異なって負のオフセットは使えず、 転送するバイト数を倍数とするオフセット値に限定されます。

レジスタオフセット

ベースレジスタ (Rn) が格納している値と、インデックス用のレジスタが 格納しているオフセット値を加えたメモリアドレスに格納されている数値を 転送先のレジスタ(Rt)にコピーします。 インデックス用レジスタ (Rm) には 32ビットレジスタ (Wm) または 64ビットレジスタを指定できます。 32ビットレジスタの場合でも 符号拡張を指定することが可能なため、負のオフセット値を使うことができます。 オフセット値は 浮動小数点レジスタのサイズにしたがって1倍(Bn)、2倍(Hn)、 4倍(Sn)、8倍(Dn)、16倍(Qn)することができます。

倍精度浮動小数点数のロードを例にすると、次のようにシフト量に #3 (8倍) を 指定できます。 Hn(#1)、Sn(#2)、Qn(#4) の場合も同様です。

  LDR   Dt, [base, Wm {, SXTW|UXTW {#0|#3}} ] 
  LDR   Dt, [base, Xm {, LSL|SXTX {#0|#3}} ] 

    Dt = memory[Xn + Rm {* 8} ]

命令中の SXTW | UXTW または LSL | SXTX は拡張(extend)指定子で、 インデックス用レジスタの値を 32ビットから 64ビットに拡張する 場合に符号拡張を行う(SXTW)かどうかを指定します。 64ビットの場合に LSL を指定すると転送バイト数倍のシフトを指定できます。 シフト量は省略可能で LSL でない場合は #0 になります。 シフト量は任意のビット数が指定できるわけでは無く、 「シフトするか/しないか」の2択です。アセンブラのオペランドの記述には 転送バイト数に応じたシフトするビット数を指定する必要があります。

命令のエンコード

表中で simm9 は9ビットの符号付イミディエート値(-256 .. +255)、 uimm12 は符号なし12ビットイミディエート値(0 .. 4095) を示します。 Rn はベースアドレスを格納する汎用レジスタ(X0..X31)または スタックポインタ(SP)、Rt はデータの転送先となるレジスタです。

アクセスするメモリのデータサイズは命令先頭の 2bit が表しています。整数ロード命令とのエンコードの違いは、bit26(V) が 1 になっている部分です。

LDR (simd) 313029 282726 252423 222120 191817 161514 131211 100908 070605 040302 0100
post index size 1 1 1 1 0 0 x 1 0 simm9 0 1 Rn Rt
pre index size 1 1 1 1 0 0 x 1 0 simm9 1 1 Rn Rt
LDUR size 1 1 1 1 0 0 x 1 0 simm9 0 0 Rn Rt
unsigned 12 size 1 1 1 1 0 1 x 1 uimm12 Rn Rt
literal opc 0 1 1 1 0 0 simm19 Rt
register size 1 1 1 1 0 0 x 1 1 Rm option S 1 0 Rn Rt
STR (simd) 313029 282726 252423 222120 191817 161514 131211 100908 070605 040302 0100
post index size 1 1 1 1 0 0 x 0 0 simm9 0 1 Rn Rt
pre index size 1 1 1 1 0 0 x 0 0 simm9 1 1 Rn Rt
STUR size 1 1 1 1 0 0 x 0 0 simm9 0 0 Rn Rt
unsigned 12 size 1 1 1 1 0 1 x 0 uimm12 Rn Rt
register size 1 1 1 1 0 0 x 0 1 Rm option S 1 0 Rn Rt

bit31 と bit30 でレジスタのサイズを決めています。128ビットのレジスタはbit23 と bit22 が 「1 1」となります。

size(31,30)opc(23,22)レジスタ名ビット
0 00 1Bn8
0 10 1Hn16
1 00 1Sn32
1 10 1Dn64
0 01 1Qn128

レジスタペアのロード/ストア命令

2つのレジスタの内容の 16 バイトのデータを1命令でメモリに読み書きできる命令です。 スタックポインタにレジスタを退避/復帰する場合によく使います。 特にスタックポインタは 16 バイトでアラインする必要がある、つまり、メモリにアクセスする場合にスタックポインタの値は16で割り切れる必要があります。 いつも 2 つのレジスタをひとまとめにして退避/復帰するほうがエラーを予防できると思います。

fldp fstp

アドレッシング

レジスタペアのロード/ストア命令では、符号付き7ビットのイミディエートオフセットアドレッシングの1種類のみです。 イミディエートオフセットには3種類あって、オフセットを先にベースレジスタに加える「プレインデックス」、 アクセス後にオフセットをベースレジスタに加える「ポストインデックス」、ベースレジスタを更新しない 「符号付オフセット」があります。

プレインデックス

プレインデックスは、ベースレジスタと呼ぶスタックポインタ (SP) または 64 ビット汎用レジスタ (Xn) が格納している アドレスに定数を加えたメモリアドレスから読み出し、 そのアドレスをベースレジスタに書き戻します。 結果として、ベースレジスタの値に定数を加えたメモリアドレス を使ってベースレジスタを「事前に」更新し、そこにアクセス していることになります。ベースレジスタを「事前に」更新する ことから「プレインデックス」と呼んでいます。 次の擬似コードでは、memory[ adr ] でメモリアドレス adr に 格納されている内容を示します。 base はベースレジスタ(Xn または SP)、Xt1、Xt2 は転送先のレジスタを 示します。 プレインデックスはオペランドの最後に「!」を付けて表します。

以下の simm7 は 7 ビットの符号付き定数 (+63..-64) にレジスタのサイズ (Snでは4バイト、Dnでは8バイト、Qnでは16バイト) を乗じた範囲の数として指定します。 したがって Sn では -256 から 252 の範囲の 4 の倍数、Dn では -512 から 504 の範囲の4の倍数、 Qn では -1024 から 1008 の範囲の 16 の倍数をオフセットとして指定します。 アセンブラが 7 ビットの符号付き定数 (+63..-64) に変換します。

  LDP St1, St2, [Xn|SP, #simm7]!     // プレインデックス
  LDP Dt1, Dt2, [Xn|SP, #simm7]!     // プレインデックス
  LDP Qt1, Qt2, [Xn|SP, #simm7]!     // プレインデックス
  STP St1, St2, [Xn|SP, #simm7]!     // プレインデックス
  STP Dt1, Dt2, [Xn|SP, #simm7]!     // プレインデックス
  STP Qt1, Qt2, [Xn|SP, #simm7]!     // プレインデックス

      base = base + simm7;
      Rt1 = memory[base];
      Rt2 = memory[base + (4|8|16)];

ポストインデックス

ポストインデックスはベースレジスタの示すメモリアドレスから 読みだした後、オフセットに指定した定数をベースレジスタに加算します。 つまり、ベースレジスタのメモリアドレスにアクセスした「後に」 ベースレジスタを別のメモリアドレスに更新することから 「ポストインデックス」と呼んでいます。

  LDP St1, St2, [Xn|SP], #simm7      // ポストインデックス
  LDP Dt1, Dt2, [Xn|SP], #simm7      // ポストインデックス
  LDP Qt1, Qt2, [Xn|SP], #simm7      // ポストインデックス
  STP St1, St2, [Xn|SP], #simm7      // ポストインデックス
  STP Dt1, Dt2, [Xn|SP], #simm7      // ポストインデックス
  STP Qt1, Qt2, [Xn|SP], #simm7      // ポストインデックス

      Rt1 = memory[base];
      Rt2 = memory[base + (4|8|16)];
      base = base + simm7;

符号付き7ビットオフセット

ベースレジスタにオフセットを加えたメモリからロードします。 プレインデックスと同じアドレスを読み書きしますが、ベースレジスタは更新しません。

  LDP St1, St2, [Xn|SP {,#simm7}]
  LDP Dt1, Dt2, [Xn|SP {,#simm7}]
  LDP Qt1, Qt2, [Xn|SP {,#simm7}]
  STP St1, St2, [Xn|SP {,#simm7}]
  STP Dt1, Dt2, [Xn|SP {,#simm7}]
  STP Qt1, Qt2, [Xn|SP {,#simm7}]

      Rt1 = memory[base + simm7];
      Rt2 = memory[base + simm7 + (4|8|16)];

命令エンコード

命令 313029 282726 252423 222120 191817 161514 131211 100908 070605 040302 0100
STP post-index opc 1 0 1 1 0 0 1 0 imm7 Rt2 Rn Rt
LDP post-index opc 1 0 1 1 0 0 1 1 imm7 Rt2 Rn Rt
STP pre-index opc 1 0 1 1 0 1 1 0 imm7 Rt2 Rn Rt
LDP pre-index opc 1 0 1 1 0 1 1 1 imm7 Rt2 Rn Rt
STP offset opc 1 0 1 1 0 1 0 0 imm7 Rt2 Rn Rt
LDP offset opc 1 0 1 1 0 1 0 1 imm7 Rt2 Rn Rt
opcbit
0 032
0 164
1 0128

オフセットの値は内部的には 7 ビットの符号付きオフセット (-64/+63) になっています。 レジスタのサイズにしたがって 4 倍 (Sn)、8 倍 (Dn)、 または 16 倍 (Qn) されて、 実際のオフセット(-256/+252、-512/+504、-1024/+1008) が使われます。 アセンブラのソースコードではレジスタのサイズ倍した値をつかいますが、アセンブラが 7 ビットの符号付きオフセットに変換します。

STP による PUSH

STP 命令をプレインデックスアドレッシングで使って、2つのレジスタの内容をスタックにプッシュします。スタックポインタを先に -16 してから、そこにレジスタの内容をコピーします。 先に指定したレジスタがメモリアドレスの小さい側にコピーされます。スタックトップは先に指定したレジスタの内容になります。

fstp_push

LDP による POP

LDP 命令をポストインデックスアドレッシングで使って、スタックの内容を2つのレジスタにポップします。 スタックポインタの示す内容を先に(前に)指定したレジスタにコピーして、そのあとで2番目のレジスタにコピーします。 最後にスタックポインタの値を +16 します。

fldp_pop

メモリをアクセスするとき、スタックポインタの値は 16 の倍数となっている必要があります。16バイトを単位としてスタックを使うと効率的なため、STP/LDP 命令で複数のレジスタを一度に退避/復帰するとスタックのメモリを有効に使うことができます。


続く...



このページの目次