先月作成した「Qt Quick 2 のスピード」という記事ですが、こちらの予想以上に読んでいただいているようです。その記事では紹介していませんでしたが、QML エンジンの内部構造については KDAB が “QML Engine Internals” というシリーズの記事を作成しています。興味の有る人は是非そちらも読んでみてください。
今回はその中の “QML Engine Internals, Part 3: Binding Types” を基に 「Qt Quick 2 のスピード」の補足をしてみようと思います。
さて、前回は Qt5 の QML でバインディングを高速化するために v4 と呼ばれるエンジンが追加されていることを紹介しました。v4 はバインディングの処理で行われている JavaScript の式の計算を行うためのエンジンですので、非常に単純な JavaScript エンジンのサブセットと言えなくもありません。しかし、フルセットの JavaScript エンジンと言えば Qt Quick 2 では V8 を使用しています。これらの使い分けはどのようになっているのでしょうか。また、JIT がありネイティブコードを実行することもある V8 よりも、(まだ)JIT の無い v4 の方が早いのは何故なのでしょうか。
Qt Quick 2 ではバインディングは QQmlAbstractBinding の派生クラスである以下のクラスのいずれかで処理されます。
- QV4Bindings::Binding
- QV8Bindings::Binding
- QQmlBinding
それぞれがどのように使い分けされているかを見ていきましょう。説明の都合上、上記とは逆順に見ていきます。
QQmlBinding
QQmlBinding は最も基本的で最も遅いバインディングです。各式の計算をその式用に作成されたコンテキストと V8 エンジンを使用して行います。QQmlBinding は QML のオブジェクトのインスタンスが生成される度に各バインディングごとにコンパイルされます。このため、各バインディングの実行の度に V8 を新たに起動することになるためそのオーバーヘッドが大きくなります。
基本的には使用頻度の少ないバインディングですが、他の式とコンテキストの共有が行えない、クロージャや eval を用いたときは必ずこのバインディングになるのでスピードが必要な場面ではそれらを避ける必要があります。
QV8Bindings::Binding
QQmlBinding では各式がそれぞれ別々にコンパイルされていましたが、QV8Bindings では対象となる QML ファイルに含まれるバインディングをまとめてコンパイルします。また、コンパイルの結果を QQmlCompiledData に保存しておいて、同じ QML ファイルから別のインスタンスを生成する際に再利用します。
QV4Bindings::Binding
QV4Bindings では V8 がネイティブコードにコンパイルするのに対して、仮想マシンのバイトコードを生成し、インタプリタで実行します。それでも QV4Bindings の方が早いのは何故なのでしょうか。一つは V8 エンジンを実行する際のオーバーヘッドです。また QML や QObject のプロパティに対する最適化もその理由の一つです。V8 エンジンは Qt とは無関係ですのでプロパティへのアクセスは式の計算時に名前解決を通して行われています。v4 は QML の為に作られたエンジンですので、式のコンパイル時にプロパティの名前解決を行い、計算時にはインデックスを用いてプロパティにアクセスします。(このため QObject のダイナミックプロパティや setContextProperty() で登録されたオブジェクトを v4 では扱えません。)また、V8 でコンパイルされた結果は最適化のために再コンパイルされることがあります。これらの結果、総合的には v4 の方が速くなります。
まとめ
KDAB のブログでこのシリーズの記事を書いている Thomas McGuire は昨年の Qt Developer Days 2012 でも QML の内部について話したセッションを行っています。そのビデオも公開されていますので、参考にしてみてください。