ラッチ──初期化は一回だけ

関数に入ったとき、それが最初のときだけある処理をしたい。そんな要求がわりとある。

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() { … }

関数ポインターをつかえると(あるいはコールバック関数などでも)、制御の流れを実行時に組み替えられるようになる感があり黒魔術。楽しい。


この記事が気に入ったらサポートをしてみませんか?