Node.js Advent Calendar 2017 25日目の記事です。トリとなります。
さて先日11/26・27日に行われたNode学園祭でv8について発表させて頂いたが、
30分という制約上色々カットせざるを得なかった。
またv8のコードを読む・コントリビュートする上で伝えられる事も色々と溜まったので一度アウトプットすることにした。
というわけでまとまりのない記事になる可能性が高いがご容赦いただけると助かります。
事前資料
以下のスライドがNode学園祭の発表資料なので読んどいていただけると理解がはやいかも
前準備
チェックアウト
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行ある。
- v8のオブジェクトモデルすべてが定義されており
- 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
- JSReceiver (suitable for property access)
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::String
やv8::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++
- C++(External Reference)
- CodeStubAssembler(CSA)
- Javascript
- pros
- JSから呼び出すのが高速
- cons
- 意図せぬ型情報の汚染が起こりうる。パフォーマンス問題も起きがち。
- セキュア(getter関数を意図せず呼び出したり、monky-patchingされてしまったり)に作るのが非常に難しい
- Compiler組み込み関数やruntime関数呼び出しが必要で、これが高コスト
- summary
- 使わないこと!
- pros
- アセンブリ
- pros
- とにかく早い
- コールスタックを操作できる
- cons
- メンテナンスコストが高すぎる
- アーキテクチャ毎に必要で大変
- summary
- 使わないこと!
- pros
という感じでコードを書く手段が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.h
にStackTrace
クラスがあるので呼び出された箇所でStackTrace st;st.Print()
を呼ぶと良い。- また
v8::Object
クラスを継承しているオブジェクトはもれなくPrint
メソッドを持っているので、a->Print()
を呼ぶと中身が見れる。
- CSA
Print()
関数がCodeStubAssembler
に定義されているので、そこにNode*
を渡すことでa->Print()
を行うコードを出力してくれる。ただし、IntPtrT
を渡すと落ちるので注意。その場合はSmiTag
すれば良い。
あとはこれを繰り返すしかない。
まとめ
なんか本当にまとまりのない文章になってしまった。申し訳ない。
まあつまりv8のコード見るのも書くのも大変だってことです。