abcdefGets

ゲッツ!

V8 javascript engineについての細かい話 (Node.js Advent Calendar 2017)

Node.js Advent Calendar 2017 25日目の記事です。トリとなります。

さて先日11/26・27日に行われたNode学園祭でv8について発表させて頂いたが、
30分という制約上色々カットせざるを得なかった。
またv8のコードを読む・コントリビュートする上で伝えられる事も色々と溜まったので一度アウトプットすることにした。
というわけでまとまりのない記事になる可能性が高いがご容赦いただけると助かります。

事前資料

以下のスライドがNode学園祭の発表資料なので読んどいていただけると理解がはやいかも

speakerdeck.com

前準備

チェックアウト

v8はGitHubに直接はホスティングされていない。
GitHub上にあるv8リポジトリはミラーで実際にはchromium.googlesource.comホスティングされている。
ただし開発の際にはGitHubリポジトリをフォークしてgit remote add fork git@github.com:<user-name>/v8.gitとかしてforkリモートを追加してやるとよい。 あとは実際にpushして保存したい場合にはこのGitHubのフォークに対して行う。
なぜこんな面倒な事をしているかというと、v8のリポジトリGitHubではないためgit pushを使わない。
git clというコマンドを利用するのだが、このコマンドでgit cl uploadとしてもGerritにレビューを送るだけでpush相当のことはできない。
その為GitHubをブランチの保存先に利用している。

さて、v8を実際に開発するためには最初にこのページを確認すると良い。
Contributing · v8/v8 Wiki · GitHub

ただしあんまり親切ではないので軽く説明すると、まず最初に以下のステップに従ってdepot_toolsをインストールする
depot_tools_tutorial(7)
インストールしてパスを通したら、gclientコマンドが叩けるか確認する。
その後コードを以下の要領でチェックアウトする。
Using Git · v8/v8 Wiki · GitHub
あとはgit checkout -b foo-branchで適当なブランチを作って作業を開始する。

ビルド

一旦チェックアウトできたらビルドをしてみる。
現在v8はGNというメタプロジェクトビルドツールを利用しており、以下のpythonコマンドでビルド設定を出力できる。

X64の例

DEBUG

./tools/dev/v8gen.py x64.debug -vv

OPT_DEBUG

./tools/dev/v8gen.py x64.optdebug -vv

RELEASE

./tools/dev/v8gen.py x64.release -vv

次にビルドを行うのだが、ビルドにはNinjaというビルドツールを利用しており、以下のコマンドでビルドが開始できる。

DEBUG

ninja -C out.gn/x64.debug

OPT_DEBUG

ninja -C out.gn/x64.optdebug

RELEASE

ninja -C out.gn/x64.release

後はビルド完了を待つだけ。
ちなみにフルビルドには2.5 GHz Intel Core i7 16GB Memoryで30分近くかかるので辛抱強く待つ。

コミット

作業が完了したらいつも通りgit add .して、git commitでコミットコメントを書く。
コードをコミットする場合にはgit cl formatで最初に全ファイルをフォーマットし、git cl uploadでコードをGerritにコミットする。
この際chromium.orgメールアドレスを持っていないとWarningが表示されるもののここはYを押して先に進む。
その後コミットメッセージの入力を求められるが、gitのコミットメッセージを利用する場合にはそのままEnterでオッケー。

レビュー

もしレビューを開始する場合にはgit cl ownersコマンドでレビュアーを探し出して、GerritのReviewsにそのレビュアーを追加して待つ。

エディタ・IDE

自分はEmacs + RTagsを利用している。 この辺の記事が参考になった。C++11時代のEmacs C++コーディング環境 - Qiita
またv8のコミッターに直接聞いたところ、CLionを使ってると言っていた。CLion: A Cross-Platform IDE for C and C++ by JetBrains
ただし、自分の環境ではしょっちゅうフリーズしていたので諦めた。
あとはvimが多いみたいだが、vimはよくわからんので頑張ってC++環境作ってください。
VSCodeは無理だった。
Atomってなんだっけ?

v8のコード構成

ディレクトリ構成概要

v8のソースコードは全てsrcディレクトリに格納されており、以下のような構成になっている。

src ---+
       |
       +---arm
       +---arm64
       +---mips
       +---mips64
A      +---ia32
       +---x64
       +---ppc
       +---s390
       +---wasm
       +---asmjs
       |
       +---ast
       +---compiler
B      +---compiler-dispatcher
       +---interpreter
       +---parsing
       |
       +---js
       +---builtins
C      +---runtime
       +---snapshot
       +---regexp
       +---profiler
       |
D      +---ic
       |
       +---heap
E      +---heap-symbols.h
       +---zone
       +---objects
       |
F      +---inspector
       |
       +---base
       +---debug
       +---tracing
       +---extensions
G      +---libplatform
       +---libsampler
       +---third_party
       +---trap-handler
       |
       +---*.cc/*.h
       .
       .
       .

ファイル数が非常に多くすべてを説明し切るのは非常に難しいので一旦概略を述べると、

  • Aグループ
    • ディレクトリ群はassemblerコードやdisassembler、macro-assembler、simulator等、CPU毎の個別コードが格納されている。
  • Bグループ
  • Cグループ
    • JSのビルトイン関数・v8内部の実行時ヘルパ関数等が格納されている。
  • Dグループ
    • Inline Cache周りのコードが格納されている。
  • E
    • オブジェクトモデルとメモリ周りのコードが格納されている。
  • F
    • インスペクタ
  • G
    • デバッグやプラットフォーム抽象化層のコードを格納している。

とこんな感じで分類できる。
ただしこれも結構苦しい分類で実際にはどこのディレクトリにも属していないsrc直下のファイルが各グループに相当するコードを実装していたりと、結構見境がない感じ。

主要ファイル

恐らくよく見ることになるソースを列挙しておく。

  • api.h/api.cc
    • Embedder向けのAPIが定義されている。
  • objects.h/objects.cc
    • v8のオブジェクトモデルすべてが定義されており objects.ccに関しては19366行ある。
  • compiler/compiler.cc
    • コンパイルのエントリポイントになるのでここからコードを追うことが多い。
  • compiler/pipeline.cc
    • compiler.ccからつながってここにたどり着く。TurboFanと呼ばれている箇所
  • runtime/runtime-*.cc
    • ランタイム関数が定義されている。ここもよく見る。
  • builtins/builtin-*.cc
    • より高速なランタイム関数群。CodeStubAssembler(あとで解説)かAssemblerで記述されている。
  • interpreter/*.cc
    • いわゆるIgnitionのコードがここに収まっている。
  • ic/*.cc
    • Inline Caching周りの実装・ランタイムが格納されている。

v8の内部実装

ここからは実際に内部で使われている主要な機能を紹介していく。

公開API

v8::HandleScope

v8のGCから割り当てられたオブジェクトをC++の世界でも監視するための仮想スコープを生成する。
下のv8::Localで詳しく解説する。

v8::Local

おそらく最もよく見ることになるクラス。
v8はGCを持っているが、C++にはGCは無い。
代わりにRAII(Resource Acquisition Is Initialization)と呼ばれるリソースの確保・破棄をメモリと紐付けて行う方法が一般的である。
C++にはデストラクタと呼ばれる、スタックに割り当てられたクラスがスコープを抜けて破棄される際に必ず呼ばれる関数があり、
そのクラスでポインタをラップすることでスコープを抜けた時に一緒にポインタを破棄するという使い方をよくする。いわゆるスマートポインタと呼ばれる機能である。
v8::Localはヒープに割り当てたオブジェクトをC++の世界で監視するためのラッパクラスで、デストラクタが呼ばれれたタイミングで現在のHandleScopeと共に削除される。

例.

void test() {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::HandleScope handle_scope;

  v8::Local<v8::Array> array = v8::Array::New(isolate, 3);
  ...
}

一度v8::HandleScopeが生成されるとv8::Localはすべてそのv8::HandleScopeに割り当てられる。
そのため、test関数が終了したタイミングでhandle_scopeのデストラクタが呼び出されると、そのv8::HandleScopeに紐づくすべてのv8::Localも同時に削除される。

v8::Handle

v8::Localでラップされているが、実際にv8::HandleScopeに紐付いているクラス。
apiによってはこっちのv8::Handleを返すやつもあるが、まあ基本的にはv8::Localと同じように使えば良い。

v8::Isolate

最初に説明すべきか悩んだが、一旦v8::Localを先に回した。
v8::Isolateはv8のコードベースを貫く基礎になる部分でかなり特殊な作りになっている。
もともとv8はstaticメソッドが非常に多く、マルチスレッドについてあまり考えていない作りになっていた。
まあ、これはChromium側でプロセス分離してv8を起動すればよかったので問題はなかったのだが。
さて、いざEmbedder側でマルチスレッド化しようとするとかなり問題を引き起こすことがわかった。
そのため、このv8::Isolateという仕組みを結構無理やり組み込んだ。

v8::Isolateがどんなものかというと、Thread Local Storage(Tls)に格納された巨大なオブジェクトになっており、
実行コンテキストに紐づくグローバルな情報をほぼ全て格納している。
Tlsに格納されているのでスレッド毎に違うv8::Isolateを透過的に持つことが可能でEmbedder側はスレッドをあまり意識せずにコードを書くことが可能になっている。
内部で利用する様々なオブジェクト(FixedArray)やHidden Classを表すMapクラス等もこのv8::Isolateから生成している。
このクラスはほぼすべての箇所に渡されていて、v8::Isolate無しではコードを書くのは難しい状態になっている。

上記のサンプルコードを再度使うと

void test() {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::HandleScope handle_scope;

  v8::Local<v8::Array> array = v8::Array::New(isolate, 3);
  ...
}

この関数でもv8::Isolateを渡しているのがわかる。
v8::Array::Newなどとしているが実際にArrayを生成しているのはv8::Isoalteである。
そのため、v8の内部ではあまりスレッドの競合について考える必要が無く、まあなかなか便利な仕組みである。

v8::internal

外部公開API以外のクラス等はすべてv8::internal名前空間に定義されている。
長いのでv8::iに省略する。

オブジェクトモデル

v8は非常に特殊な作りになっており、C++の中に独自のオブジェクトモデルを作り出している。
そのオブジェクトモデルはsrc/objects.hの冒頭コメントに記載されており、
それを簡略化して抽出するとこうなる。

  • Object
    • Smi (immediate small integer)
    • HeapObject (superclass for everything allocated in the heap)
      • JSReceiver (suitable for property access)
        • JSObject
        • JSProxy
      • FixedArrayBase
        • ByteArray
        • BytecodeArray
        • FixedArray
        • FixedDoubleArray
      • Name
        • String
        • Symbol
      • HeapNumber
      • BigInt
      • Cell
      • PropertyCell
      • PropertyArray
      • Code
      • AbstractCode, a wrapper around Code or BytecodeArray
      • Map
      • Oddball
      • Foreign
      • SmallOrderedHashTable
      • SharedFunctionInfo
      • Struct
      • WeakCell
      • FeedbackVector

v8::i::Objectを基底クラスとしたオブジェクトツリーを作り出しており、
v8内部で使用されるほぼすべてのクラスはv8::i::Objectを継承している。javaみたいだね。
こういうわかりやすいヒエラルキーがあるのでさぞ読みやすいかと思いきや、コードは非常に複雑で...まあ読みやすくはない。
v8はこのオブジェクトモデルをうまく機能させるためにC++のやり方に従わない。
どういうことかと言うとこれらのクラスはフィールドをC++のクラスを通じて実装していない。
これらのクラスはあくまでメモリレイアウトをC++の世界で表現するためだけに存在しており、すべてのフィールドはthisポインタに対して直接オフセット指定して取得している。
つまりC++のオブジェクトレイアウトを無視して、自分たちで完全にメモリレイアウトをコントロールしている。
擬似コードで表現すると以下のようになる。

class SomeObject {
  Value* get_field1() {
    char* self = reinterpret_cast<char*>(this);
    self += header_offset;
    return Value::Cast(self);
  }
  void Initialize() {
    char* self = reinterpret_cast<char*>(this);
    self += header_offset;
    *self = Smi::Cast(1);
  }
};
static const size_t OBJECT_SIZE = sizeof(char) * 32;
SomeObject* object = reinterpret_cast<SomeObject*>(malloc(OBJECT_SIZE));
object->Initialize();
object->get_filed1(); // 1

この様に自分でフィールドのオフセットを制御している。
さて、このオブジェクト階層の頂点からチェックしていこう。
まずv8::i::Objectは以下の2つに分岐する。

  • Smi
    • 31ビット整数、ポインタアドレスの末尾は常に0
    • ヒープに確保されることはない
  • HeapObject
    • 4バイトアライメントの32ビットポインタ。アドレス末尾は1
    • ヒープに確保されたオブジェクト。GCの対象になる。

HeapObject

まずはv8::i::HeapObjectから。
v8::i::Objectは上述のように直接メモリレイアウトをいじっているので
HeapObjectを継承したオブジェクトはフィールドにアクセスする場合には以下のようなマクロを使っている。

#define FIELD_ADDR(p, offset) \
  (reinterpret_cast<byte*>(p) + offset - kHeapObjectTag)

#define READ_FIELD(p, offset) \
  (*reinterpret_cast<Object* const*>(FIELD_ADDR_CONST(p, offset)))

// こちらはGCのConcurrentマーキングがONになっている場合にアトミックにフィールドを更新するために  
// AtomicWordを使ったバージョンと通常のものに分岐している。
#ifdef v8_CONCURRENT_MARKING
#define WRITE_FIELD(p, offset, value)                             \
  base::Relaxed_Store(                                            \
      reinterpret_cast<base::AtomicWord*>(FIELD_ADDR(p, offset)), \
      reinterpret_cast<base::AtomicWord>(value));
#else
#define WRITE_FIELD(p, offset, value) \
  (*reinterpret_cast<Object**>(FIELD_ADDR(p, offset)) = value)
#endif

SMI_ACCESSORS(FixedArrayBase, length, kLengthOffset)

#define SMI_ACCESSORS_CHECKED(holder, name, offset, condition) \
  int holder::name() const {                                   \
    DCHECK(condition);                                         \
    Object* value = READ_FIELD(this, offset);                  \
    return Smi::ToInt(value);                                  \
  }                                                            \
  void holder::set_##name(int value) {                         \
    DCHECK(condition);                                         \
    WRITE_FIELD(this, offset, Smi::FromInt(value));            \
  }

// 実際には以下のように展開される。

int FixedArrayBase::length() const {
  DCHECK(condition);
  Object* value = (*reinterpret_cast<Object* const*>(
  reinterpret_cast<const byte*>(this) + kLengthOffset - kHeapObjectTag)
  return Smi::ToInt(value);
}

int FixedArrayBase::set_length(int value) const {
  DCHECK(condition);
  base::Relaxed_Store(
      reinterpret_cast<base::AtomicWord*>(
          reinterpret_cast<byte*>(this) + kLengthOffset - kHeapObjectTag);
      reinterpret_cast<base::AtomicWord>(Smi::FromInt(value)));
}

重要なのは、

reinterpret_cast<const byte*>(this) + kLengthOffset - kHeapObjectTag

の部分で、thisポインタに特定のフィールドのオフセットを足したあと、kHeapObjectTagを引いているのがわかる。
ちなみにkHeapObjectTagの定義は以下。

const int kHeapObjectTag = 1

ただの1、つまりポインタアドレス末尾に1を立てるだけ。
v8::i::HeapObjectは割り当て時にkHeapObjectTag分多めに確保してから割り当てる。
以下がサンプルコード

#include <stdio.h>
#include <stdlib.h>
#include <iostream>

const int kHeapObjectTag = 1;
const int kHeapObjectTagSize = 2;
const intptr_t kHeapObjectTagMask = (1 << kHeapObjectTagSize) - 1;

inline static bool HasHeapObjectTag(const char* value) {
  return ((reinterpret_cast<intptr_t>(value) & kHeapObjectTagMask) ==
          kHeapObjectTag);
}

int main() {
  auto allocated = reinterpret_cast<char*>(
      malloc(sizeof(char) * (2 + kHeapObjectTag)));
  auto heap_object = allocated + kHeapObjectTag;
  heap_object[0] = 'm';
  heap_object[1] = 'v';
  printf("%ld %ld %p %p %d\n", reinterpret_cast<intptr_t>(allocated),
         reinterpret_cast<intptr_t>(heap_object), allocated, heap_object,
         HasHeapObjectTag(heap_object));
  free(allocated);
}

実行すると私の環境では以下の結果になる。

140289524108464 140289524108465 0x7f97b3400cb0 0x7f97b3400cb1 1
見事にアドレスの末尾1が立っている。

またv8::i::HeapObjectは自分自身の型を識別するためにHidden Classを表すv8::Mapオブジェクトを先頭に持っている。

そのためv8::i::HeapObjectもメモリレイアウトは以下のようになる。

+-----+-----------------------+--------+
| Map | Derived Object Header | values |
+-----+-----------------------+--------+

必ず先頭に型を表すv8::Mapを持っているため、そこを見ればv8::i::HeapObjectの型がわかる。
またDerived Object Headerと書かれている部分は継承先のオブジェクトによって異なる(v8::i::FixedArrayならlengthフィールドだったり)。

以下がMapとJSObjectを簡略化した表したC++コード

#include <stdio.h>
#include <stdlib.h>
#include <iostream>

const int kHeapObjectTag = 1;
const int kHeapObjectTagSize = 2;
const intptr_t kHeapObjectTagMask = (1 << kHeapObjectTagSize) - 1;

inline static bool HasHeapObjectTag(const char* value) {
  return ((reinterpret_cast<intptr_t>(value) & kHeapObjectTagMask) ==
          kHeapObjectTag);
}

class Map {
 public:
  enum InstanceType {
    JS_OBJECT,
    JS_ARRAY,
    JS_STRING
  };

  void set_instance_type(InstanceType instance_type) {
    instance_type_ = instance_type;
  }

  InstanceType instance_type() {
    return instance_type_;
  }
 private:
  InstanceType instance_type_;
};

const int kHeaderSize = sizeof(Map);
typedef char byte;
typedef char* Address;

class HeapObject {
 public:
  char value() {return reinterpret_cast<Address>(this)[0];}
  Map::InstanceType instance_type() {
    return reinterpret_cast<Map*>(
        reinterpret_cast<Address>(this) - kHeaderSize)->instance_type();
  }
  void Free() {
    auto top = reinterpret_cast<Address>(this) - kHeaderSize - kHeapObjectTag;
    free(top);
  }
 protected:
  static Address NewType(Map::InstanceType instance_type, size_t size) {
    auto allocated = reinterpret_cast<Address>(
        malloc(sizeof(byte) * (size + kHeaderSize + kHeapObjectTag)));
    auto map = reinterpret_cast<Map*>(allocated);
    map->set_instance_type(instance_type);
    return allocated + kHeaderSize + kHeapObjectTag;
  }
};

class JSObject: public HeapObject {
 public:
  static JSObject* New() {
    auto a = NewType(Map::JS_OBJECT, 1);
    a[0] = 'o';
    return reinterpret_cast<JSObject*>(a);
  }
};

class JSArray: public JSObject {
 public:
  static JSArray* New() {
    auto a = NewType(Map::JS_ARRAY, 1);
    a[0] = 'a';
    return reinterpret_cast<JSArray*>(a);
  }
};

class JSString: public JSObject {
 public:
  static JSString* New() {
    auto a = NewType(Map::JS_STRING, 1);
    a[0] = 's';
    return reinterpret_cast<JSString*>(a);
  }
};

int main() {
  JSObject* objects[] = {
    JSObject::New(),
    JSArray::New(),
    JSString::New()
  };

  for (int i = 0; i < 3; i++) {
    auto o = objects[i];
    switch (o->instance_type()) {
      case Map::JS_OBJECT:
        printf("JSObject => %c\n", o->value());
        break;
      case Map::JS_ARRAY:
        printf("JSArray => %c\n", o->value());
        break;
      case Map::JS_STRING:
        printf("JSString => %c\n", o->value());
        break;
    }
  }

  for (int i = 0; i < 3; i++) {
    objects[i]->Free();
  }
}

実行するとJSObject => o, JSArray => a, JSString => sが出力される。
かなり例が巨大になってしまったが、これでヒープに割り当てられたオブジェクトの型を正確に分類できているのがわかると思う。

さらにSmiが何かを説明しよう。

Smi

SmiはSmall Integerの略で、31ビットまでの整数を直接ポインタ領域に確保する。
またRubyでも同じ手法が取られているようだ。
通常ポインタはそれだけで32ビットCPUなら4バイト、64CPUなら8バイトを使う。
つまり31ビットまでの整数ならば直接ポインタの代わりに格納できるわけだ。
このようにポインタ領域に確保してヒープを使わないことでメモリ節約・高速化の両方を成し遂げている。

さてどのように格納するかというと直接int値をreinterpret_cast<T*>してポインタに変換してしまう。

class Smi {
 public:
  static Smi* FromInt(int value) {
    return reinterpret_cast<Smi*>(value);
  }

  int value() {
    return reinterpret_cast<intptr_t>(this);
  }
};

Smi* NewSmi(int value) {
  return Smi::FromInt(value);
}

int main() {
  printf("%d %d\n", NewSmi(120)->value(), NewSmi(110)->value());
  // out 120 110
}

上記の例を見ていただければわかるだろう。
さらにv8::i::HeapObjectの場合には下位1ビットを立てたが、Smiの場合には末尾0をタグとして利用しているので、
キャストしただけで直接数値演算が可能である。そのためオーバーヘッドもない。
また64ビットCPUならばポインタは64ビットなのでより大きな整数が格納できるが、32ビットとの互換性のため31ビットの領域しか使用しない。
64ビットの場合のビットレイアウト

+----------------+-----------------+------------------+
| 31 bit integer | 32 bit zero bit | 1 bit smi tag(0) |
+----------------+-----------------+------------------+

単純に下位32ビットをゼロ埋めするだけ。

JSReceiver

プロパティアクセス可能なJSオブジェクトを表す。つまりほぼすべてのJSオブジェクトを表す。
JSReceiverの下には上述のJSObjectが存在し、これがjsのObjectクラスを表す。
さらにJSObjectの下には以下のような階層がある。

  • JSArray
  • JSArrayBuffer
  • JSArrayBufferView
    • JSTypedArray
    • JSDataView
  • JSBoundFunction
  • JSCollection
    • JSSet
    • JSMap
  • JSStringIterator
  • JSSetIterator
  • JSMapIterator
  • JSWeakCollection
    • JSWeakMap
    • JSWeakSet
  • JSRegExp
  • JSFunction
  • JSGeneratorObject
  • JSGlobalObject
  • JSGlobalProxy
  • JSValue
    • JSDate
  • JSMessageObject
  • JSModuleNamespace
  • WasmInstanceObject
  • WasmMemoryObject
  • WasmModuleObject
  • WasmTableObject

これらのv8::i::JS~クラスは我々EmbedderがAPI経由で利用するv8::Stringv8::Array等のクラスの本当の姿で、
v8::String等のクラスはただのラッパークラスでしかない。
実際の実装はすべてv8::i::JS~クラスが持っている。

FixedArrayBase

v8で頻出するクラスであるv8::i::FixedArrayのベースとなる実装。
v8は内部のいたるところでこの固定長配列を利用しており、何度もお目にかかることになる。 v8::i::FixedArrayは更に以下の様な階層を持っている。

  • DescriptorArray
  • FrameArray
  • HashTable
    • Dictionary
    • StringTable
    • StringSet
    • CompilationCacheTable
    • MapCache
  • OrderedHashTable
    • OrderedHashSet
    • OrderedHashMap
  • Context
  • FeedbackMetadata
  • TemplateList
  • TransitionArray
  • ScopeInfo
  • ModuleInfo
  • ScriptContextTable
  • WeakFixedArray
  • WasmSharedModuleData
  • WasmCompiledModule

特にv8::i::DescriptorArrayはプロパティディスクリプタを格納している配列で、
プロパティアクセスの際によく取得する。

オブジェクトに関しては1記事では限界があるので次に進む。
次はv8内部のコード生成部分に関して

CodeGeneration of v8

v8はいくつかのコード生成方法がある。
v8-devグループでスライドがあったのでそれを参考にまとめると、

  • C++
    • cons
      • C++から呼び出すのが高速
      • 実行も高速
      • 比較的読みやすい
      • 拡張、デバッグがやりやすい
      • メモリがすべてのIsolateを通して共有されるためOSがメモリ不足になった場合に破棄される。
    • pros
      • JSから呼び出すのが遅い
    • summary
      • 巨大な関数やパフォーマンスクリティカルではない箇所には良い
  • C++(External Reference)
    • runtimeではなく直接C++の関数を登録する方式で登録した関数しか呼び出せないが直接C関数として呼べるので高速
    • cons
      • JSから呼び出すのが通常のC++と比べて早い
    • pros
      • allocationができない
      • 初期化が結構面倒
    • summary
      • 頻繁に呼ばれるような小さな関数には便利
  • CodeStubAssembler(CSA)
    • pros
      • JSから呼び出すのが早い、JSに戻るのも早い
      • 実行も高速
    • cons
      • 冗長
      • デバッグが厳しい
      • C++から呼び出すと遅い
      • isolate毎にメモリを圧迫する
    • summary
      • 小さな関数やパフォーマンスが必要な箇所に向いている
  • Javascript
    • pros
      • JSから呼び出すのが高速
    • cons
      • 意図せぬ型情報の汚染が起こりうる。パフォーマンス問題も起きがち。
      • セキュア(getter関数を意図せず呼び出したり、monky-patchingされてしまったり)に作るのが非常に難しい
      • Compiler組み込み関数やruntime関数呼び出しが必要で、これが高コスト
    • summary
      • 使わないこと!
  • アセンブリ
    • pros
      • とにかく早い
      • コールスタックを操作できる
    • cons
    • summary
      • 使わないこと!

という感じでコードを書く手段がC++, C++(Ex)、CSA、javascriptアセンブリの約5種類ある。
javascriptアセンブリはすでに新規で書かれることはほぼない。
以前はv8はjavascriptでランタイムを書いていた時期があったがほぼなくなった。
現在は低速でも問題ない箇所や新たに実装されるSpecに関してはC++で、それ以外のパフォーマンスが必要な箇所に関してはCSAで記述される。

さて次はCSAを解説する。

CodeStubAssembler (CSA)

v8の内部で利用されるDSL言語。
実はv8ではすでに新しくアセンブリ言語を記述することはあまりない。
その代わりアセンブリを出力するCSAを記述することでよりメンテナンス性が高く高速なコードを出力することができる。

以下にCSAの例を示す。

function fibonacci(num){
  var a = 1, b = 0, temp;
  const result = [];

  while (num >= 0){
    result.push(a);
    temp = a;
    a = a + b;
    b = temp;
    num--;
  }

  return result;
}

C言語入門 - フィボナッチ数の計算 - サンプルプログラム - Webkaru

このフィボナッチ数を計算して配列に格納するjavascript関数をCSAに変換すると以下のようなコードになる。

TNode<JSArray> Fibonacci(TNode<Context> context) {
  TVARIABLE(var_a, MachineType::PointerRepresentation(), IntPtrConstant(0));
  TVARIABLE(var_b, MachineType::PointerRepresentation(), IntPtrConstant(1));
  TVARIABLE(var_temp, MachineType::PointerRepresentation());
  TVARIABLE(var_index, MachineType::PointerRepresentation());

  Node* fixed_array = AllocateFixedArray(PACKED_ELEMENTS, IntPtrConstant(11),
                           INTPTR_PARAMETERS, kAllowLargeObjectAllocation)

  Label loop(this), after_loop(this);

  Branch(IntPtrGreaterThan(IntPtrConstant(100), var_index), &loop, &after_loop);
  BIND(&loop);
  {
    StoreFixedArrayElement(fixed_array, SmiTag(var_index), var_a,
                           SKIP_WRITE_BARRIER);
    var_temp.Bind(var_a);
    var_a.Bind(IntPtrAdd(var_a, var_b));
    var_b.Bind(var_temp);
    Increment(&var_index, 1);
    Branch(IntPtrGreaterThan(IntPtrConstant(100), var_index),
           &loop, &after_loop);
  }
  BIND(&after_loop);
  Node* native_context = LoadNativeContext(context);
  Node* array_map = LoadJSArrayElementsMap(PACKED_ELEMENTS, native_context);
  Node* array = AllocateUninitializedJSArrayWithoutElements(
      array_map, SmiConstant(12), nullptr);
  StoreObjectField(array, JSArray::kElementsOffset, fixed_array);
  return array;
}

ある程度抽象化されているとは言えどうしても冗長なコードになってしまうが、アセンブリよりは遥かに読みやすいのではないだろうか?
これらは直接値を出力するのではなく、実行予定ツリーを組み立ててからツリーを巡回し、一つ一つの命令が自動的にアセンブリに置き換えられる。
ちなみにコンパイルしていないのでもしかしたらコンパイルエラーが出るかもしれない。

長くなりすぎたので内部の解説はこのあたりで終了する。

コードを読む

v8のコードを読むのは非常に面倒だが、いくつか方法がある。
まずはそれぞれのエディタのコードジャンプを使う。
まあこれでとりあえず定義とかはある程度見れるはず。
ただしv8は大量のマクロを使っており、クラスの関数定義すらマクロで行う場合があるので、どうしても定義が見つからない場合にはfind | grepも駆使したほうが良い。
またマクロで文字列結合されている場合もあるので、find src | grep '##FooBar'みたいな感じで##を使ってgrepする必要があるかもしれない。

デバッグする

コードを読んでも実行時の状態がわからなかったり呼び出し階層がわからない場合もあるので、ランタイムでは以下の方法でチェックすると良い。

  • C++
    • src/base/debug/stack_trace.hStackTraceクラスがあるので呼び出された箇所でStackTrace st;st.Print()を呼ぶと良い。
    • またv8::Objectクラスを継承しているオブジェクトはもれなくPrintメソッドを持っているので、a->Print()を呼ぶと中身が見れる。
  • CSA
    • Print()関数がCodeStubAssemblerに定義されているので、そこにNode*を渡すことでa->Print()を行うコードを出力してくれる。ただし、IntPtrTを渡すと落ちるので注意。その場合はSmiTagすれば良い。

あとはこれを繰り返すしかない。

まとめ

なんか本当にまとまりのない文章になってしまった。申し訳ない。
まあつまりv8のコード見るのも書くのも大変だってことです。