マイクロスレッド基本おさらい(2)
上記仮定の下で、exec(), suspend(), resume()は約こんなコードでいいはず。
void exec(TCtx *p_ctx, void (*bar)()) { PUSH S1 PUSH S2 ... PUSH Sm PUSH PR p_ctx->ssp = p_ctx->stk; swap(SP, &(p_ctx->ssp)); PR = bar; J PR } void suspend(TCtx *p_ctx) { PUSH S1 PUSH S2 ... PUSH Sm PUSH PR swap(SP, &(p_ctx->ssp)); POP PR POP Sm ... POP S2 POP S1 J PR } void resume(TCtx* p_ctx) { (suspend()と同じ) }
ここで、
- TCtx: ctxの型(sspとstk[]をメンバとしてもつ)
- SP: スタックポインタ
- PR: 関数からの戻り値を保持するレジスタ(既出)
- S1〜Sm: 汎用レジスタR1〜Rnのうち、関数呼び出し規約により、関数呼び出し前後で保存される(せねばならない)もの
- PUSH
: レジスタ の値をスタックにpushする - POP
: スタックに退避された値をスタックからpopしてレジスタ にロードする - J
: レジスタ に格納されたアドレスにジャンプする - swap(SP, p): pをSPの型へのポインタとして、SPと*pの値を互いに交換する
Cとアセンブリ言語がちゃんぽんになったような怪しい仮想コードだが、意味は多分明白だと思う、、
(ので構文要素についてのこれ以上の説明はしない。)
果たしてこんなロジックで動くのか?
(つづく)
…いやちょっとその前に、p_ctx->ssp = p_ctx->stkやswap()の実現方法と“J PR”についてちゃんと考えとこう、、
【p_ctx->ssp = p_ctx->stkの実現方法】
これは、空いてる汎用レジスタRt,Ruを使って次のように書けるはず。
Rt = &(p_ctx->ssp) /* (1) */ Ru = &(p_ctx->stk) /* (2) */ Rt = Ru; /* (3) */
(1)の時点でp_ctxの値はレジスタに乗っている(∵仮定2.)ので、(1)はRt=p_ctx+(定数)という単なるadd命令で済む。(2)も同様。(3)はメモリへのレジスタ内容のストア。実現性は問題ないはず。レジスタRt, Ruが破壊される件については、そのタイミングがPUSH後かつPOP前なので問題ないはず。
【swap()の実現方法】
swap()は、空いてる汎用レジスタRtを使って次のように書けるはず。
Rt = p_ctx->ssp /* (1) */ p_ctx->ssp = SP /* (2) */ SP = Rt /* (3) */
(1)は、p_ctxをベースアドレスとする固定ディスプレースメントなインデックス参照、かつ、p_ctxの値はレジスタに乗っている(∵仮定2.)。(2)も同様。(3)は単なるレジスタ間のmoveとしてそれぞれ実現される。いずれもCが走るまっとうなCPUなら備えてるはずの命令の範囲内なので、実現性は問題ないはず。レジスタRtが破壊される件については、そのタイミングがPUSH後かつPOP前なので問題ないはず。
【“J PR”を使う関数呼び出し規約】
仮定3.により、今考えている呼び出し規約では、“J PR”が関数からのreturnとして働く(通常は)。この規約の下で、関数呼び出しは次のロジックにより実現される(bar()を呼び出す例)。
Rt = (bar()のアドレス) PR = PC + s /* (4) */ J Rt /* (5) */
(4)の直後、PRは(5)の“J Rt”の直後(+CPUによっては遅延スロットサイズ)を指し、(5)の“J Rt”でbar()が呼ばれる*1。
で、多重呼び出し(と呼び出し先からのリターン)も併せると、結局次のようなロジックとなる。
foo() { ... Rt = (bar()のアドレス) PR = PC + s J Rt ... } bar() { PUSH PR ... Rt = (baz()のアドレス) PR = PC + s J Rt ... POP PR J PR } baz() { ... J PR }
このように多重呼び出しも矛盾無くできる。
(つづく)