JavascriptのObjectリテラルとJSON.parseについて
V8のJSON.parseについて
最近(ちょっと前か)話題のオブジェクトリテラルよりもJSON.parse
のほうが早い件について。
その理由を内部実装の観点から書く。
また注意点を最後に書いた。
話題のブログは以下から https://v8.dev/blog/cost-of-javascript-2019
パースについて
V8はjavascriptコードをパースするにあたって、Lazy Parseを行っている。
Lazy Parse
javascriptはブラウザという特殊な環境で実行される言語であるため、パースにも少々工夫が必要になる。
基本的にV8はすべてのソースコードをパースしない。
一旦グローバルスコープにあるものだけをちゃんとパースして、それ以外はPreParserというパーサで関数だけをかき集める。
function foo() { } function bar() { function baz() { const value = 100; } }
この例だと、foo
とbar
、baz
という関数が存在していることはパースするが、const value = 100
は一切パースせずASTを生成しない。
baz
が呼び出されて初めてconst value = 100
がパースされる。
How to skip parsing
ではどのようにコードのパースをスキップしているかというと、V8のパーサは手書きの再帰下降構文解析器になっており、
ParserFunction
のようなメソッドが一杯あるクラスになっている。
さらにパーサはPreParser
とParser
に分かれており、それをテンプレート引数で受け取って実行するParserBase
クラスが各パースのエントリーポイントを定義している
template <typename Impl> class ParserBase<Impl> { ... protected: ... ExpressionT ParseFunctionExpression(); ... private: Impl* impl() {return parser_;} Impl* parser_; };
のような感じでParserBase
がパーサのBNFに対応する各パース段階のエントリーポイントを持つ。
その中でimpl()->ParserFunctionLiteral()
のような形で実際のパーサを呼び出してパース処理を行っている。
Parser and PreParser
さて実際にパース処理を行うパーサは2種類ある。
PreParser
はASTを生成せずに関数名やその他情報を集めるだけのパーサで、Parser
は実際にASTの生成を行うパーサとなっている。
ここで大事なのがPreParser
の実装である。
PreParser
はASTは生成しないものの実際に構文のパースは行う。
そのため、V8ではjavascriptのソースコードは2回パースされることとなる。
ここでJSON.parse
の特殊性が影響してくる。
JSON.parse
JSON.parse
の第一引数には文字列のJSONオブジェクトが渡される。
これがキモになる。何故かと言うと、文字列のパースコストは非常に低いのだ。
なぜなら文字列はパース段階ではなく、スキャン段階でトークン化されるため、何度パースされても文字列リテラルのトークンを判定するだけで処理が完了する。
そのため、パースの負荷が非常に低くなる。
また、JSON.parse
自体もランタイムで処理が行われるために、実際に呼び出されるまで処理が行われず、不要なパースをすべてスキップすることができる。
すなわち手動でLazy Parsingをしているに等しい状態となっている訳だ。
これらの要因が組み合わさってオブジェクトリテラルをべた書きするよりも、JSON.parse
の方が早くなるという不思議な現象が発生する。
注意点
ただしこの方法には注意点があって、あくまでこの手法はstartup timeの高速化しかできない。
実際の実行時間はJSON.parseのほうが遅くなるので、FMPの高速化には寄与するかもしれないが、すべてのオブジェクトリテラルをこれで置き換えると結構遅くなりそうなので注意。
あくまでパースのLazy化と考えたほうが良い。
ちなみにJSON.parse
が遅いのはASTを作らず、毎回パースの手間がかかるから。
一度しか実行されないケースに関してはそこまで速度の差はでない。
一応ベンチマーク
https://jsperf.com/json-parse-vs-object-literal-in-parser
自分の環境では80%くらいJSON.parse
が遅い