分岐命令の意味論一覧

機械語には複数種類の分岐命令があり、しかも一つの分岐命令でも異なる意味論で使ったりします。 どういう意味論を持っているか、まとめてみました。

以下では、無条件分岐命令か、あるいは条件分岐命令で条件が成り立った場合の意味論についてまとめます。 条件分岐命令で条件が不成立だった場合(単に次の命令に進むだけでいつもとおなじ)は除きます。

一つの関数内で完結するもの

無条件分岐

特定の番地にプログラムの制御を移す、もっとも単純な分岐命令です。 一つの関数内で完結することから、分岐元と分岐先のアドレスの差は小さいことが多く、相対アドレス指定が行われることが多いです。

ジャンプテーブルを使った多方向分岐

C言語switch文や、関数型言語のパターンマッチ等を効率的に実装する場合などに出現します。 汎用レジスタに書かれている値への分岐命令として実現されます。 原理的には相対アドレス指定でも問題ないのですが、そうなっている命令セットはあまり見かけません。

関数間にまたがるもの

関数呼び出し

特定の番地にプログラムの制御を移しますが、呼び出し先ルーチンが終了した場合にこの命令の次の命令から再開する、という機能も兼ね備えた分岐命令です。 一命令に複数の機能がありますが、非常に頻出であるため、RISCであっても採用される命令です。 関数呼び出し命令をどのように実装するかは、命令セットによっていろいろな方法が模索されましたが、どの方法も欠点があります。 RISC-Vでは、呼び出し先関数のアドレス指定に、相対アドレス指定を採用しています。これだと遠いアドレスにある関数が呼び出せないように思えますが、相対アドレスの絶対値が大きい場合には、二命令かけて相対アドレスを指定する、という方法をとっています。

間接関数呼び出し

関数呼び出しとほぼ同じですが、呼び出す関数が動的に決まるというものです。例えば、関数ポインタを使った関数呼び出しや、C++の仮想関数呼び出しなどです。

関数からの復帰

関数を終了し、関数呼び出し命令の次の命令に制御を移す命令です。 「関数呼び出し命令の次の命令」のアドレスは、絶対アドレスで保存されているはずです。

末尾呼び出し

特定の番地にプログラムの制御を移しますが、呼び出し先ルーチンが終了した場合にこの関数に戻るのではなく、この関数を呼び出した命令の次の命令から再開するような分岐命令です。 やたらに複雑な命令のように見えますが、(呼び出し規約を無視すれば)無条件分岐命令とまったく同じ動作になっています。 呼び出し先関数のアドレスの指定方法は、関数呼び出しと同じになります。

コルーチン呼び出し

コルーチンは、関数の一般化です。 コルーチンと関数とで異なるのは、以下の二点です。 関数の場合、常に最後まで実行されたのち、呼び出し元に帰ってきます。一方、コルーチンの場合、その途中の任意の場所で実行を中断し、呼び出し元に帰ってくる可能性があります。 関数の場合、次回呼び出し時もまた先頭から実行します。一方、コルーチンの場合、次回呼び出し時は、その中断したところから再開します。

こんな複雑なことを実現する分岐命令も、RISC-Vには存在します。中断したアドレスは、必然的に絶対アドレスが使われます。

なお、コルーチンからの復帰というのは、別のコルーチンの呼び出しに相当するため、ここに含まれます。

機械語を意識して表にまとめてみる

表にまとめてみると、以下のようになります。参考のため、RISC-Vの分岐命令(jjaljrjalrtailcallret)を併記しました。

ただし、以下でRASというのは、return address stackの略で、関数呼び出しに関連する命令の分岐予測を高精度に行うハードウェアのことです。 関数呼び出しに関連するアドレスだけが入れられる、汎用でないレジスタだと思ってもおおよそ間違っていません。

一つの関数内で完結する分岐 関数をまたぐ分岐で、RASに書き込まない 関数をまたぐ分岐で、RASに書き込む
無条件分岐(j 末尾呼び出し(j 関数呼び出し(jal 分岐先が近く、固定
N/A 末尾呼び出し(tail 関数呼び出し(call 分岐先が遠く、固定
ジャンプテーブル分岐(jr 間接末尾呼び出し(jr 間接関数呼び出し(jalr 分岐先が不定
N/A 関数からの復帰(ret コルーチン呼び出し/復帰(jalr RASを読み出し、そこに分岐

継続をあらわに扱った場合

先の表は、機械語・ハードウェアを強く意識したものになっていました。継続を使うと、抽象的なままで考えることができます。

関数からの復帰は、継続の末尾呼び出しなので、間接末尾呼び出しと同じはずです(実際、retjrの特殊な場合です)。 また、ジャンプテーブルも、複数の継続の中から一つの継続を選んで呼び出す、と解釈できるため、やはり間接末尾呼び出しと同じになっています。

継続渡しスタイルに変換してしまえば、無条件分岐と末尾呼び出しの区別はなくなります。また、関数はコルーチンの退化したバージョンに過ぎません。

これに加え、分岐先が遠い・近いというのは完全に機械語の制約であり、本質的なものではないことに注目すれば、分岐命令は本質的に以下のように分類されるということがわかります。

  • 静的に決まっている継続の末尾呼び出し(j
  • 動的に決まる継続の末尾呼び出し(jr
  • 現在の継続を取り出しつつ、静的に決まっている継続を呼び出し(jal
  • 現在の継続を取り出しつつ、動的に決まる継続を呼び出し(jalr