コルーチンの実装 ~ setjmp/longjmp

コルーチンが実装したい!理由は様々ですが人は誰しもそう思うときがあるでしょう。この記事はどうやってコルーチンを実装するかのメモその1です。

stack{less,ful} coroutine

まずコルーチン界隈を検索すると、スタックレス・スタックフルコルーチンという言葉が出てきて混乱します。スタックがなければローカル変数を持てないような気がするわけですがじゃあスタックレスってなんだと思うわけです。

実は、いろんな記事や実装を眺めてみますと、「スタックレス」というのは単に「コールスタックを保存しない」というだけで、データの置き場としてのスタックフレームはどこかに確保している、というのが実態のようです。「スタックレス」は、コールスタックを確保しないことで必要なメモリ量を静的に見積もれるというのがうまみでしょうか。というかまあ言ってしまえばスタックレスコルーチンは「ローカル変数をインスタンス変数にし、yieldごとにメソッドを切り分けた(あるいは状態機械とした)」インスタンスメソッドのsyntax sugarであると言ってよさそうです。話がずれますが、Rustのasync/awaitの文脈でコルーチンが言及されるのもまさにこの理由だな~と今日ふと思いました。スタックフルコルーチンでは、使うかどうかわからんスタックフレームをコンパイラが気を利かせて適当に確保しなければいけないわけですが、こういう「よきにはからえ」的機能は低レベル言語と大変相性がよろしくなさそうです。

さて、一方のスタックフルコルーチンは、「普通」のコルーチンです。つまり、関数のように振る舞い、他の関数なども呼べるものです。ただし、スタックフレームをまるっと保存できるような点が普通の関数とことなるわけですね。

どうやってスタックフレームを保存するか

次の問題は、じゃあどうやってスタックフレームを保存してyieldするかということでしょう。ナイーブに考えると、マクロみたいなのを定義してあげて。

#define YIELD(val, col) (\
__asm__("mov qword ptr [%0], rip\n\t"\
        "mov qword ptr [%0 + 0x8], rax\n\t"\
        //...\
: : "r"(col));\
col.data = val;\
__asm__(\
"jmp %0"\
: : "g"(col.caller)\
)\
)

とかやればいいのかもしれませんが、めんどくさいしコンパイラの最適化一発で(特にripをいじったりどこにjmpするか決めるあたり)ぶっ壊れそうな香りがぷんぷんしています。実は、これをどうにかする方法がsetjmp/longjmpなのです!

(次回へ続く)

Comments

comments powered by Disqus