RISC系の命令セットでは、一命令でレジスタに書き込める即値と、一命令では書き込めない即値が存在する設計になっています。 一命令では書き込めない即値の場合、二命令以上かけて即値を生成することになるわけですが、そこに存在する罠に引っかかった記録です。
なんでそんな仕組みになっているのか
RISC系の命令セットでは、一命令の長さを固定(ほとんどの場合、4Byte)にすることが基本です。 レジスタ幅が32bit(=4Byte)だとすると、一命令32bitの中に32bit分の即値を埋め込むことは明らかに不可能です。
一命令の長さをもっと長く、例えば8Byteにすれば入るじゃないか、という感じですが、そうするとプログラムのサイズがほぼ二倍に増えてしまい、受け入れがたいという設計方針のようです。
一命令の長さを可変にすればいいじゃないか、というのはプロセッサの設計が複雑化するため、やはり受け入れがたいというのがRISCの設計方針です。
MIPSの例
MIPSの命令セットでは、LUI
命令(命令中の16bit値を16bit左シフトした値をデスティネーションレジスタに書き込む命令)とORI
命令(命令中の16bit値をゼロ拡張した値とソースレジスタの値のビット毎論理和をデスティネーションレジスタに書き込む命令)を組み合わせ、任意の32bit即値を作ることができます。
例えば、0x12345678
をt0
レジスタに書き込みたいときは、
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
命令の即値は符号拡張される
の二点です。
符号拡張される点が厄介で、例えば0x89abcdef
をt0
レジスタに書き込みたいとき、
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
命令を組み合わせても作れない範囲の数である0x000000007ffffabc
をt0
レジスタに書き込みたいときのことを考えてみます。
まず、LUI
命令では下位12bitに触ることができませんので、ADDI
命令の即値は必然的に0xabc
になります。
この値は符号拡張すると0xfffffffffffffabc
になるので、余分な0xfffffffffffff
を打ち消すために、LUI
命令では1
だけずれた値を作る必要があります(32bit版の時と同じ論理です)。
よって、LUI
命令で0000000080000000
をt0
レジスタに書き込めばよさそうです。
しかし、これは命令セットの都合上不可能です。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が符号拡張され、0x000000007ffffabc
がt0
レジスタに書き込まれ、目的が達成されます。
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
命令(二つのソースレジスタの値rs1
、rs2
を二の補数表現で表された符号付き64bit整数として比較し、rs1 < rs2
なら分岐する命令)や、BLTU
命令(二つのソースレジスタの値rs1
、rs2
を符号なし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命令が使われることを確認しています