ラッチ──初期化は一回だけ
関数に入ったとき、それが最初のときだけある処理をしたい。そんな要求がわりとある。
static bool initialized = false;
if (!initialized) {
// 初期化処理
initialized = true;
}
// 通常処理
これで十分目的は達しているのだけれど、まだコードがアセンブラで書かれていた古い時代に「自己書きかえ」という黒魔術があったという話がいまだに心に残っている。
前掲の疑似コードは(あまり意味はないけれど)次のように書きかえられる。
static bool initialized = false;
check:
if (!initialized) goto init;
process:
...
return;
init:
...
initialized = true;
goto process;
ところで goto はアセンブラでは jump や branch といった、アドレスを伴う1ワードの命令に相当する。この知識ともうひとつ、アセンブラには nop という「なにもしない」命令があるという知識をつかうと、このコードは次のように書きかえられる。
check:
goto init;
process:
...
return;
init:
...
__asm__ {
load @check, $nop
load @check+1, $nop
}
goto process;
(__asm__ ブロックは check のテキストアドレスに nop 命令の値を書き込むというイメージで読んでください😁)
つまり初期化処理で入口のジャンプを nop に塗りつぶす。すると次回以降、条件分岐なしで process に飛び込めるというもの。(かっこいい……!)
この nop の書き換えを「ラッチをかける」と言ったりもしたとか。
現代のプログラムではコード領域は読みとり専用になっているため「自己書きかえ」の黒魔術は使えなくなってしまった。実際に経験できなくて残念。
ところで現代でも「関数ポインター」で(毎回の)条件分岐を省略できる。条件分岐は CPU の投機的実行パイプラインをストールさせる重めの処理だと言われるので、条件分岐をなくせるのは魅力的(強弁)。
static void init();
static void process();
static void (*action)() = init;
void f() {
action();
}
void init() {
action = process; // 次回以降 process を呼ぶよう action を変更
… // 初期化処理
process();
}
void process() { … }
関数ポインターをつかえると(あるいはコールバック関数などでも)、制御の流れを実行時に組み替えられるようになる感があり黒魔術。楽しい。