2008-09-08

V8 祭りつづき

前回の続きです. コードは飽きないうちに読め. これまでのあらすじ: プロパティアクセスを速くしたいから JIT をしようぜ.

コンパイラ概観

V8 のコンパイラは JavaScript の AST を機械語に変換する. (AST はパーサがつくる.) AST のツリー構造は, Node クラスのサブクラス一族で構成されている (ast.h) コンパイラは関数の AST である FunctionLiteral オブジェクトをうけとって Code オブジェクトを生成する. AST とコンパイラは(またしても) Visitor パターンでつながる. (Visitor 好きは Strongtalk からの伝統らしい. Strongtalk VM のコンパイラも同じようなことをしている. 20 世紀の残り香が...)

AST 側は Vistor のインターフェイスを提供する:

//ast.h
class Visitor BASE_EMBEDDED {
...
//ここで VisitXxx がバババと定義される.
#define DEF_VISIT(type)                         \
  virtual void Visit##type(type* node) = 0;
  NODE_LIST(DEF_VISIT)
#undef DEF_VISIT
...
};

//ここで VisitXxx がバババと定義される.
class Node: public ZoneObject {
 public:
  Node(): statement_pos_(kNoPosition) { }
  virtual ~Node() { }
  virtual void Accept(Visitor* v) = 0; // Visitor の受け口
  ...
};

コンパイラ側のトップレベルクラスである CodeGenerator は Visitor を実装する.

//codegen.h
class CodeGenerator: public Visitor {
  ...
};

各アーキテクチャ毎に CodeGenerator のサブクラスがあり, そのサブクラスで VisitXxx() を実装する. 以下は IA32 の If の例:

//codegen-ia32.cc
...
#define __  masm_->
...
void Ia32CodeGenerator::VisitIfStatement(IfStatement* node) {
  Comment cmnt(masm_, "[ IfStatement");
  // Generate different code depending on which
  // parts of the if statement are present or not.
  bool has_then_stm = node->HasThenStatement();
  bool has_else_stm = node->HasElseStatement();

  if (FLAG_debug_info) RecordStatementPosition(node);
  Label exit;
  if (has_then_stm && has_else_stm) {
    Label then;
    Label else_;
    // if (cond)
    LoadCondition(node->condition(), CodeGenState::LOAD, &then, &else_, true);
    Branch(false, &else_);
    // then
    __ bind(&then);
    Visit(node->then_statement());
    __ jmp(&exit);
    // else
    __ bind(&else_);
    Visit(node->else_statement());

  } else if (has_then_stm) {
    ...
  }

  // end
  __ bind(&exit);
}

冒頭のお茶目なマクロはさておき, やりたいことはわかるかんじ.

プロパティアクセス

今回の主題はプロパティアクセスなので, 関数定義とかホスト側からの呼び出しなんかは省略して本題に進もう. AST 上では Property クラスがプロパティアクセスをあらわしている.

class Property: public Expression {
 public:
  Property(Expression* obj, Expression* key, int pos) // pos は先頭からの文字数(行数?)
      : obj_(obj), key_(key), pos_(pos) { }
  ...
};

Visit の実装:

void Ia32CodeGenerator::VisitProperty(Property* node) {
  Comment cmnt(masm_, "[ Property");

  if (is_referenced()) { // 大抵はこちらに入る.
    __ RecordPosition(node->position()); // 今は無視してね.
    AccessReferenceProperty(node->key(), access()); // ここに進む
  } else { // こっちに来ても, 最終的には上のパスに入る.
    ASSERT(access() != CodeGenState::STORE);
    Reference property(this, node);
    __ RecordPosition(node->position());
    GetValue(&property);
  }
}

ようやく本題:

void Ia32CodeGenerator::AccessReferenceProperty(
    Expression* key,
    CodeGenState::AccessType access) {
  Reference::Type type = ref()->type();
  ...
  // load か store か (=右辺か左辺か)
  bool is_load = (access == CodeGenState::LOAD ||
                  access == CodeGenState::LOAD_TYPEOF_EXPR);

  if (type == Reference::NAMED) { // foo.bar というタイプのアクセス.
    // Compute the name of the property.
    Literal* literal = key->AsLiteral();
    Handle<String> name(String::cast(*literal->handle())); // "bar" という文字列(シンボル).

    // Call the appropriate IC code.
    if (is_load) {
      // 読み出しのためのランタイム LoadIC_Initialize (?) を呼び出すコード片を取得する.
      // Code はコードをあらわすオブジェクトで, 関数として呼び出せる.
      Handle<Code> ic(Builtins::builtin(Builtins::LoadIC_Initialize));
      Variable* var = ref()->expression()->AsVariableProxy()->AsVariable(); // 変数 (foo) の取得
      // Setup the name register.
      __ Set(ecx, Immediate(name)); // プロパティ名 (bar) を引数に詰み...
      if (var != NULL) {
        ASSERT(var->is_global());
        // 取得したコード片を呼び出す.
        // receiver は既にスタックに積んであることを期待している.
        __ call(ic, code_target_context);
      } else {
        __ call(ic, code_target); // 上とほぼ同じ. (relocation のモードが違うだけ.)
      }
    } else {
      // 値を store するケース. load の逆になる. 今回は省略.
    }
  } else { // foo[bar] というタイプのアクセス. 今回は省略.
    // Access keyed property.
    ....
  }
  __ push(eax);  // IC call leaves result in eax, push it out
}
// assembler-ia32.cc
void Assembler::call(Handle<Code> code,  RelocMode rmode) {
  EnsureSpace ensure_space(this);
  last_pc_ = pc_;
  ASSERT(is_code_target(rmode));
  EMIT(0xE8);
  emit(reinterpret_cast<intptr_t>(code.location()), rmode);
}

call() は call 命令を書き出しているだけ. 要するに(receiver がスタックにいること期待しつつ)プロパティのキーを積み, 何かの関数を呼び出している. Builtins::builtin(Builtins::LoadIC_Initialize) で戻るコードの中身が肝のようだ.

Builttins::builttin() 関数はあらかじめ用意された Code オブジェクトを返している.

 
 // builtins.h
 static Code* builtin(Name name) {
   // Code::cast cannot be used here since we access builtins
   // during the marking phase of mark sweep. See IC::Clear.
   return reinterpret_cast<Code*>(builtins_[name]);
 }

この Code オブジェクトは LoadIC::GenerateInitialize() 関数がつくる.

// ic.cc
void LoadIC::GenerateInitialize(MacroAssembler* masm) {
  Generate(masm, ExternalReference(IC_Utility(kLoadIC_Miss)));
}

C++ のくせに python/scheme ばりの高階関数ワールドになってきた. ここでは LoadIC::Miss() 関数をあわらす定数を引数に LoadIC::Generate() を呼び出す. ここでようやく何かしらのコードが生成される:

void LoadIC::Generate(MacroAssembler* masm, const ExternalReference& f) {
  // ----------- S t a t e -------------
  //  -- ecx    : name
  //  -- esp[0] : return address
  //  -- esp[4] : receiver
  // -----------------------------------

  __ mov(eax, Operand(esp, kPointerSize));

  // Move the return address below the arguments.
  __ pop(ebx);
  __ push(eax);
  __ push(ecx);
  __ push(ebx);

  // Perform tail call to the entry.
  __ TailCallRuntime(f, 2);
}

レシーバ, プロパティ名をスタックに積み, 更に MacroAssembler::TailCallRuntime() を呼び出す. ここでは引数の個数(2)を eax につめて...

void MacroAssembler::TailCallRuntime(const ExternalReference& ext,
                                    int num_arguments) {
  mov(Operand(eax), Immediate(num_arguments));
  JumpToBuiltin(ext);
}

ようやくどこかに jmp している.... CEntryStub は先に積んだスタックやレジスタを整理して ebx の関数ポインタを呼び出し可能な状態にするコードを吐く. コードの中身は本題に関係ないので省略します.


void MacroAssembler::JumpToBuiltin(const ExternalReference& ext) {
  // Set the entry point and jump to the C entry runtime stub.
  mov(Operand(ebx), Immediate(ext));
  CEntryStub ces;
  jmp(ces.GetCode(), code_target);
}

jmp なあたりが tail call なんだろうか.

それにしても機械語はややこしくて疲れるね... AccessReferenceProperty() のあたりから復習すると, ここでは LoadIC_Miss() を呼びだすべく, 以下のようなコードを生成していた:

instruction cache

結局, 実行時に呼ばれるのは LoadIC_Miss() (を呼び出すコード片) ということになる. この LoadIC_Miss() はどんな関数だろう.

Object* LoadIC_Miss(Arguments args) {
  NoHandleAllocation na;
  ASSERT(args.length() == 2);
  LoadIC ic;
  IC::State state = IC::StateFrom(ic.target(), args[0]);
  return ic.Load(state, args.at<Object>(0), args.at<String>(1));
}

LoadIC::Load() を呼び出すだけのアダプタだった. LoadIC::Generate() が生成するコードは, 呼び出し先の関数が Argument 引数をとることを前提に, Argument を引数に渡すようなコード片をつくる. この Argument に詰め込まれた引数の実体をとりだすのが LoadIC_Miss() の仕事だった.

実質的な仕事(=プロパティアクセス)をする LoadIC::Load() は...:

// ic.cc
Object* LoadIC::Load(State state, Handle<Object> object, Handle<String> name) {
  ... // エラーチェック

  if (FLAG_use_ic) {
   ... // receiver が String/Array/Function だった場合の特別ケース. 今回は省略
  }

  // 添字が整数値だった場合の特別パス.
  // (整数値をキーとしたプロパティは通常のプロパティとは別扱いされ, element フィールドに保存されている.)
  uint32_t index;
  if (name->AsArrayIndex(&index)) return object->GetElement(index);

  /*
   * ここからが本題
   */

  // Named lookup in the object.
  LookupResult lookup;
  object->Lookup(*name, &lookup);

  // If lookup is invalid, check if we need to throw an exception.
  if (!lookup.IsValid()) {
    ... // エラー処理
  }

  // Update inline cache and stub cache.
  if (FLAG_use_ic && lookup.IsLoaded()) {
    UpdateCaches(&lookup, state, object, name);
  }
  ...
  // Get the property.
  return object->GetProperty(*object, &lookup, *name, &attr);
}

あら. JIT したコードも 結局は Object::GetProperty() を呼び出している...

が, その手前で呼ばれている UpdateCaches() にトリックがある. UpdateCaches() は改めてコード生成をしている:

void LoadIC::UpdateCaches(LookupResult* lookup,
                          State state,
                          Handle<Object> object,
                          Handle<String> name) {
  ...
  Handle<JSObject> receiver = Handle<JSObject>::cast(object);

  // Compute the code stub for this load.
  Object* code = NULL;
  if (state == UNINITIALIZED) {
    ...
  } else {
    // Compute monomorphic stub.
    switch (lookup->type()) {
      case FIELD: { // プロパティが properties 配列に保存されたオブジェクトの場合
                    // 新ためて何かのコードを生成する
        code = StubCache::ComputeLoadField(*name, *receiver,
                                           lookup->holder(),
                                           lookup->GetFieldIndex());
        break;
      }
      case ...
        ...
      default:
        return;
    }
  }

  ...

  // Patch the call site depending on the state of the cache.
  if (state == UNINITIALIZED || state == PREMONOMORPHIC ||
      state == MONOMORPHIC_PROTOTYPE_FAILURE) {
    set_target(Code::cast(code)); // 初回はこっちに進む
  } else if (state == MONOMORPHIC) {
    set_target(megamorphic_stub()); // こっちに進む場合は後述.
  }
  ...
}

生成したコードは, IC::set_target() (IC は LoadIC の親クラス.) でどこかにセットされている. どこにセットしたかはさておき, まずどんなコードを生成しているのか見てみよう.

Object* StubCache::ComputeLoadField(String* name,
                                    JSObject* receiver,
                                    JSObject* holder,
                                    int field_index) {
  Code::Flags flags = Code::ComputeMonomorphicFlags(Code::LOAD_IC, FIELD);
  Object* code = receiver->map()->FindInCodeCache(name, flags); // 既に生成されたコードがないか調べる
  if (code->IsUndefined()) { // なければ生成
    LoadStubCompiler compiler;
    code = compiler.CompileLoadField(receiver, holder, field_index);
    ...
    Object* result = receiver->map()->UpdateCodeCache(name, Code::cast(code)); // 保存
    ...
  }
  return Set(name, receiver->map(), Code::cast(code)); // 別の場所にも保存. 用途は後述. 戻り値は code.
}

実際の生成を請け負うのは LoadStubCompiler::CompileLoadField(). コードをみてもいいかげんしんどいので, 生成されるコードを疑似コードで書いてみる:

  if (this typeof int)
    goto miss;
  if (this->map != receiver->map) // "receiver" 変数はコード生成時のレシーバ. "this" は実行時のレシーバ.
    goto miss;
  return this->properties[index];
miss:
  return LoadIC_Miss(...);

コード中の "this" や "this->receiver", そして "index" 変数は, コード生成時にわかっている定数である点に注目してほしい. 要するに, 最初の呼び出し時(=コード生成時)と同じ Map をもつオブジェクトかどうかをチェックして, もしそうならオブジェクトの properties フィールドを直にロードし, オフセット(配列)アクセスをして値を返す, というコードを出力する. チェックに失敗したら例の(遅いパスである) LoadIC_Miss() に進む. レシーバがグローバル変数の場合や, プロパティがプロトタイプチェインの上流にあったものの場合, チェックはもう少し複雑なものになる.

さて, めでたく配列アクセスを行うネイティブコードが生成されたけれど, 今は実行時, プロパティアクセスの最中で, LoadIC::Load() の中にいることを思いだしてほしい. プロパティアクセスの最中にプロパティアクセスのコードを出力している. なんだそりゃ. トリックは先に見過してきた IC::set_target() にある.

class IC {
  ...
  // Set the call-site target.
  void set_target(Code* code) { SetTargetAtAddress(address(), code); }
}
...
void IC::SetTargetAtAddress(Address address, Code* target) {
  ASSERT(target->is_inline_cache_stub());
  Assembler::set_target_address_at(address, target->instruction_start());
}
...
void Assembler::set_target_address_at(Address pc, Address target) {
  int32_t* p = reinterpret_cast<int32_t*>(pc);
  *p = target - (pc + sizeof(int32_t));
  CPU::FlushICache(p, sizeof(int32_t));
}

IC::set_target() は, Assembler のメソッドで機械語を書き替えている. set_target_address_at() の "pc" 引数(=IC::address() の戻り値)は, 今実行しているの関数 LoadIC_Miss() の呼出元のスタックフレームから 読み出したプログラムカウンタの値が入っている. このプログラムカウンタは, LoadIC_Miss() を呼び出す call 命令のあたりを指している.

つまり, LoadIC_Miss() 関数はその内部で自分の呼出元を書き換え, 速いバージョンのコードに差し替えているというわけ. この書き換えの仕組みが, V8 の instruction cache の核となっている. どのへんが cache かというと, call の operand に速いバージョンのアドレスを "cache" しているからかな. (命名は Salltalk80 の実装 に由来しているようだ. この記事の中では "inline caching" と呼んでいるけどね.)

どうやってスタックフレームを読むのか, プロトタイプの処理や変数をつかったプロパティアクセス (例:foo[bar]), メソッド呼び出しがどうなるのかなど, 細かい話は色々ある. でも色々ありすぎるので割愛させていただきます.

多態なケースへの対応

さて, ここにも気になることがある. プロパティの receiver が多態性をもっているとき; つまり receiver の Map が呼出し毎にかわるとき, 素朴につくると毎回キャッシュミスがおこる. コード生成が逆に速度の足をひきずってしまう.

V8 の instruction cache は, そういう時もしぶとく喰いさがる. UpdateCaches() の後半, 生成したコードとは別のパスがあったのを思いだそう.

void LoadIC::UpdateCaches(LookupResult* lookup,
                          State state,
                          Handle<Object> object,
                          Handle<String> name) {
  ....
  if (state == UNINITIALIZED || state == PREMONOMORPHIC ||
      state == MONOMORPHIC_PROTOTYPE_FAILURE) {
    set_target(Code::cast(code));
  } else if (state == MONOMORPHIC) {
    set_target(megamorphic_stub()); // こっちのことね
  }
  ...
}

この "state == MONOMORPHIC" のパスに入るのは, いま cache されているコードが "MONOMORPHIC" であるとき, つまり単一の型向けのコードであるときだ. MONOMORPHIC なコードの実行中に UpdateCaches() にいるなら, receiver の型が変化してキャッシュミスがおきている. つまり今動いているのは複数の型のオブジェクトを受け付ける, 現実に多態性を持つコードだということがわかる. その時, V8 は 多態をうけつけるバージョンのコードを生成する. 呼び出し元はこのコードに差し替えられる:

megamorphic_stub() から呼ばれるコード生成のメソッド:

void LoadIC::GenerateMegamorphic(MacroAssembler* masm) {
  // ----------- S t a t e -------------
  //  -- ecx    : name
  //  -- esp[0] : return address
  //  -- esp[4] : receiver
  // -----------------------------------

  __ mov(eax, Operand(esp, kPointerSize));

  // Probe the stub cache.
  Code::Flags flags = Code::ComputeFlags(Code::LOAD_IC, MONOMORPHIC);
  StubCache::GenerateProbe(masm, flags, eax, ecx, ebx);

  // Cache miss: Jump to runtime.
  Generate(masm, ExternalReference(IC_Utility(kLoadIC_Miss)));
}

StubCache::GenerateProbe() で何か悪足掻きするコードを用意して, そこでうまくいかなかったらまた LoadIC_Miss() に落ち着く, というコードを出力するようだ.

どんな悪足掻きをするのか. 疲れたのでコードの引用はさぼるけれど, StubCache::ComputeLoadField() の最後で生成したコードを保存していたのを思いだそう. Map のポインタとプロパティ名をキーに Code を保存していた. GenerateProbe() では, 渡ってきた receiver の Map とプロパティ名から, 保存しておいた Code オブジェクトを検索する. そして見つかったコードを実行している. ハッシュの検索を機械語で書くなんておぞましい. ただ幸いこれはキャッシュに過ぎないから, 開番地だの連鎖だの面倒なことはしていない. まあ十分複雑だけどね...

雰囲気を味わうためにハッシュを検索する部分だけ引用してみる. 引数の offset がハッシュ値(の modulo) で, これは手前で計算している.

static void ProbeTable(MacroAssembler* masm,
                       Code::Flags flags,
                       StubCache::Table table,
                       Register name,
                       Register offset) {
  ExternalReference key_offset(SCTableReference::keyReference(table));
  ExternalReference value_offset(SCTableReference::valueReference(table));

  Label miss;

  // Save the offset on the stack.
  __ push(offset);

  // Check that the key in the entry matches the name.
  __ cmp(name, Operand::StaticArray(offset, times_2, key_offset));
  __ j(not_equal, &miss, not_taken);

  // Get the code entry from the cache.
  __ mov(offset, Operand::StaticArray(offset, times_2, value_offset));

  // Check that the flags match what we're looking for.
  __ mov(offset, FieldOperand(offset, Code::kFlagsOffset));
  __ and_(offset, ~Code::kFlagsTypeMask);
  __ cmp(offset, flags);
  __ j(not_equal, &miss);

  // Restore offset and re-load code entry from cache.
  __ pop(offset);
  __ mov(offset, Operand::StaticArray(offset, times_2, value_offset));

  // Jump to the first instruction in the code stub.
  __ add(Operand(offset), Immediate(Code::kHeaderSize - kHeapObjectTag));
  __ jmp(Operand(offset));

  // Miss: Restore offset and fall through.
  __ bind(&miss);
  __ pop(offset);
}

えらく面倒なことをしてるんだなー程度の理解で良いのではないでしょうか.

実験

で, この instruction cache の効き目を実験する...のは, ちょっと難しい. うまく VM をだましてキャッシュミスをおこすコードがすぐには思いつかない. とりあえず MONOMORPHIC のケースの MEGAMORPHIC のケースを比べてみよう.

MONOMORPHIC:

function Hello() { this.foo = 1; this.bar = 2; }

var n = 0;
var arr = [new Hello(), new Hello()];

for (var i=0; i<10000; i++) {
  for (var j=0; j<10000; j++) {
    var obj = arr[(i+j)%2];
    n += obj.foo + obj.bar;
  }
}

print(n);

実行.

omo@contentiss:~/src/v8/trunk$ time ./shell hello.js
300000000

real    0m31.466s
user    0m31.438s
sys     0m0.000s

31.5 秒.

MEGAMORPHIC:

function Hello() { this.foo = 1; this.bar = 2; }
function Bye() { this.foo = 1; this.bar = 2; }

var n = 0;
var arr = [new Hello(), new Bye()];

for (var i=0; i<10000; i++) {
  for (var j=0; j<10000; j++) {
    var obj = arr[(i+j)%2];
    n += obj.foo + obj.bar;
  }
}
print(n);

実行.

omo@contentiss:~/src/v8/trunk$ time ./shell hello.js
300000000

real    0m36.185s
user    0m36.158s
sys     0m0.012s

36.2 秒.

このうちプロパティアクセス以外の部分でかかっている時間は...:

function Hello() { this.foo = 1; this.bar = 2; }
function Bye() { this.foo = 1; this.bar = 2; }

var n = 0;
var arr = [new Hello(), new Hello()];

for (var i=0; i<10000; i++) {
  for (var j=0; j<10000; j++) {
    var obj = arr[(i+j)%2];
    n += 1;
  }
}
omo@contentiss:~/src/v8/trunk$ time ./shell hello.js
100000000

real    0m28.045s
user    0m28.034s
sys     0m0.004s

28.0 秒.

というわけで, プロパティアクセスの速度は (31.4-28.0)/(36.2-28.0) = 0.41. MEGAMORPHIC のコードより MONOMORPHIC のコードの方が倍以上速いことがわかった. どっちもランタイム呼び出しに比べたらカスみたいなものだろうけどね.

...そのあとちょっと調べてみたら "--nouse-ic" というオプションがあった.

MONOMORPHIC の例を動かしてみると:

omo@contentiss:~/src/v8/trunk$ time ./shell --nouse-ic hello.js
300000000

real    5m35.973s
user    5m34.097s
sys     0m1.264s

あらら...ループだけだと:

omo@contentiss:~/src/v8/trunk$ time ./shell --nouse-ic hello.js
100000000

real    2m13.345s
user    2m13.320s
sys     0m0.016s

なんだか色々 nouse になってしまうフラグみたいだなあ. (コード内では FLAG_use_ic が false になる.) 感覚的には JIT off みたいなものかもしらん.

おまけ:

#include <cstdio>

struct Hello {
  int foo, bar;
  Hello() : foo(1), bar(2) {}
};

int main() {
  int n = 0;
  Hello* arr[2];
  arr[0] = new Hello();
  arr[1] = new Hello();
  for (int i=0; i<10000; i++) {
    for (int j=0; j<10000; j++) {
      Hello* obj = arr[(i+j)%2];
      n += obj->foo + obj->bar;
    }
  }

  delete arr[0], arr[1];
  printf("%d", n);
  return 0;
}
omo@contentiss:~/src/v8/trunk$ time g++ -O0 hello.cc

real    0m0.129s
user    0m0.096s
sys     0m0.036s
omo@contentiss:~/src/v8/trunk$ time ./a.out
300000000
real    0m1.000s
user    0m0.992s
sys     0m0.000s

他意しかありません.

まとめと所感

プロパティアクセスを題材に V8 の JIT を眺め, 以下のようなアイデアで instruction cache が実装されているのを確かめました.

TraceMonkey と比べてどうかというと, 動的型付けの言語に対する JIT という視点では TraceMonkey の(元ネタである Tracing Tree アルゴリズム) の方が筋は良いと思う. Tracing JIT も instruction cache も, 型チェックをしてから特定の型専用のコードを実行するという点では変わらない. Sun の Self でも, 似たような手法を "message splitting" と呼んで紹介していた.

メソッド単位でチェックを行う instruction cache や message splitting と違い, Tracing Tree はツリーのルートから葉まで, 一度チェックしたオブジェクトの型は保存されている. だから型チェックの回数をへらすことができる. これはループなどでは特に差がでるところだと思う. ループの前にチェックできる可能性があるからね.

一方で map transition は他の JS 処理系にはないアイデアなのだと思う. (ちゃんと調べてないけど.) 逆に map transition に類する暗黙の型付けの仕組を導入すれば, SpiderMonkey など他の処理系も高速化の余地がある.

V8 はこの他にも色々な工夫がある. 大きな目玉以外にも, よく使われる型(Array,String,...)を特別扱いしてチューニングするなど, 細々とした工夫が多い. 速度のために手を汚すことを厭わない気概を感じる. まあもうちょっと清潔感を持ってほしい気もするけれど.

そんな V8 に刺激をうけて他の処理系が速くなれば, ユーザとしてはハッピーに違いない. なにしろ Google Chrome には Autopagerizedelicious extension もないからなあ... などとウェブっ子ぶったところで祭りはおしまいです.