64bit版RISC-Vで即値を生成するときの罠

RISC系の命令セットでは、一命令でレジスタに書き込める即値と、一命令では書き込めない即値が存在する設計になっています。 一命令では書き込めない即値の場合、二命令以上かけて即値を生成することになるわけですが、そこに存在する罠に引っかかった記録です。

なんでそんな仕組みになっているのか

RISC系の命令セットでは、一命令の長さを固定(ほとんどの場合、4Byte)にすることが基本です。 レジスタ幅が32bit(=4Byte)だとすると、一命令32bitの中に32bit分の即値を埋め込むことは明らかに不可能です。

一命令の長さをもっと長く、例えば8Byteにすれば入るじゃないか、という感じですが、そうするとプログラムのサイズがほぼ二倍に増えてしまい、受け入れがたいという設計方針のようです。

一命令の長さを可変にすればいいじゃないか、というのはプロセッサの設計が複雑化するため、やはり受け入れがたいというのがRISCの設計方針です。

MIPSの例

MIPSの命令セットでは、LUI命令(命令中の16bit値を16bit左シフトした値をデスティネーションレジスタに書き込む命令)とORI命令(命令中の16bit値をゼロ拡張した値とソースレジスタの値のビット毎論理和をデスティネーションレジスタに書き込む命令)を組み合わせ、任意の32bit即値を作ることができます。

例えば、0x12345678t0レジスタに書き込みたいときは、

LUI   t0, 0x1234
ORI   t0, t0, 0x5678

のような命令列になります。上位16bitと下位16bitに分ければいいだけなので、非常に簡単です。

32bit版RISC-Vの例

32bit版RISC-Vの命令セットでは、LUI命令(命令中の20bit値を12bit左シフトした値をデスティネーションレジスタに書き込む命令)とADDI命令(命令中の12bit値を符号拡張した値とソースレジスタの値のビット毎論理和をデスティネーションレジスタに書き込む命令)を組み合わせ、任意の32bit即値を作ることができます。

MIPSの命令セットの場合と異なる点は、

  • 上位16bitと下位16bitに分割するのではなく、上位20bitと下位12bitに分割する
  • ADDI命令の即値は符号拡張される

の二点です。

符号拡張される点が厄介で、例えば0x89abcdeft0レジスタに書き込みたいとき、

LUI   t0, 0x89abd
ADDI  t0, t0, 0xdef

のような命令列で作る必要があります。LUIの即値が、作りたい即値0x89abcdefの上位20bitをそのまま切り出した値ではなく、それより1だけ大きい値になっている点に注意してください。 ADDI命令の即値0xdefは、符号拡張すると0xfffffdefになるので、余計な0xfffffを打ち消すために1だけずれた値を作る必要があります。

64bit版RISC-Vの例

64bit版RISC-Vの命令セットでは、LUI命令(命令中の20bit値を符号拡張し12bit左シフトした値をデスティネーションレジスタに書き込む命令)とADDI命令(命令中の12bit値を符号拡張した値とソースレジスタの値の加算結果をデスティネーションレジスタに書き込む命令)を組み合わせ、任意の符号付き32bit即値を作ることができ……ません。

この二命令で作れる値の範囲は、[-231-2048, 231-2048)の範囲の数です。符号付き32bit値の範囲は[-231, 231)なので、微妙にずれています。

なぜこんなことになってしまうのでしょうか?

符号付き32bit値の範囲に収まっており、かつLUI命令とADDI命令を組み合わせても作れない範囲の数である0x000000007ffffabct0レジスタに書き込みたいときのことを考えてみます。

まず、LUI命令では下位12bitに触ることができませんので、ADDI命令の即値は必然的に0xabcになります。 この値は符号拡張すると0xfffffffffffffabcになるので、余分な0xfffffffffffffを打ち消すために、LUI命令では1だけずれた値を作る必要があります(32bit版の時と同じ論理です)。 よって、LUI命令で0000000080000000t0レジスタに書き込めばよさそうです。

しかし、これは命令セットの都合上不可能です。LUI命令は命令中の20bit値を符号拡張するため、その20bitの中の最上位ビットが1となる正の数(0x0000000080000000)をレジスタに書き込むことはできません。

解決法

32bit符号付き整数に収まる値を生成したい場合、LUI命令とADDIW命令を使うのが正解です。ADDIW命令は、ADDI命令と似ていますが、加算結果の下32bitを符号拡張した値をデスティネーションレジスタに書き込む命令です。これを使うと0x000000007ffffabcを以下のように作成できます。

LUI   t0, 0x80000
ADDIW t0, t0, 0xabc

まず、最初のLUI命令で、t0レジスタには0xffffffff80000000が書き込まれます。次のADDIW命令では、t0レジスタに入っている値である0xffffffff80000000と、即値を符号拡張した値である0xfffffffffffffabcが加算され、0xffffffff7ffffabcが途中結果として得られます。そして、下32bitが符号拡張され、0x000000007ffffabct0レジスタに書き込まれ、目的が達成されます。

gccでは

gccでは、極力64bit命令を使いたいらしく、問題となる[231-2048, 231)の区間の即値を作る場合、LUI命令とXORI命令を組み合わせたコードが出力されます(gcc7.1.1で確認)。

LUI   t0, 0x80000
XORI  t0, t0, -1348

これはなるほどという感じですが、この命令列に特にメリットがあるようには思えません。逆に、C.ADDIW命令に圧縮する機会を失っているという明確なデメリットがあります*1

余談:ほかの命令ではこのような問題は発生しないのか

先ほどの例では、64bit用の命令であるADDI命令ではなく、32bit用の命令であるADDIWを使うことで問題が解決されました。

しかし、32bit用の対応する命令がない命令も多く存在します。

例えば、BLT命令(二つのソースレジスタの値rs1rs2を二の補数表現で表された符号付き64bit整数として比較し、rs1 < rs2なら分岐する命令)や、BLTU命令(二つのソースレジスタの値rs1rs2を符号なし64bit整数として比較し、rs1 < rs2なら分岐する命令)には、32bit版がありません。

これらは大丈夫なのでしょうか?

実は、不思議なことに大丈夫なようになっています。

まず、RISC-Vでは、32bit値は64bitレジスタ中に符号拡張された表現で入ることになっています。よって64bit値とみなして符号付き比較を行ってもまったく問題ありません。

これだと符号なし比較の方はまずそうですが、実は大丈夫です。なぜなら、符号拡張された表現は以下のようになるため、これを符号なし比較すれば大小関係は一致するからです。

32bit値 符号拡張された64bit値
0x00000000 0x0000000000000000
0x00000001 0x0000000000000001
0x00000002 0x0000000000000002
: :
0x7ffffffe 0x000000007ffffffe
0x7fffffff 0x000000007fffffff
0x80000000 0xffffffff80000000
0x80000001 0xffffffff80000001
: :
0xfffffffe 0xfffffffffffffffe
0xffffffff 0xffffffffffffffff

他にも、ビット毎論理演算命令は32bit版がありませんが、符号拡張していれば上位33bitには全く同じビットが入っているため、結果は32bit版も64bit版も同じになります。

まとめ

64bit版RISC-Vにおいて32bit符号付き整数をレジスタに書き込みたいとき、LUI命令とADDIW命令を使うのが最も楽です。LUI命令とADDI命令の組み合わせでは[231-2048, 231)の範囲の値が生成できないという罠があります。

*1:このコード例では即値が大きすぎて圧縮することは不可能ですが、即値が非常に小さい場合でもXORI命令が使われることを確認しています