LuneScript のトランスコンパイル時間を 1157 パーセント改善した件

Page content

前回から引き続き LuneScript のトランスコンパイル時間短縮を行なっています。

今回の時間短縮は以下の通りです。

lua go lua/go
改善前(6e5661a9) 25.69 sec 5.84 sec 440%
改善後(364095ef) 17.42 sec 2.22 sec 785%
改善率(改善前/改善後) 147% 263%

この表は、セルフホスティングしているソースのトランスコンパイル時間の計測結果を 示しています。 lua VM で動作させた lnsc と、go でビルドした lnsc で計測しています。

改善前の 6e5661a9 は、5/6 のバージョンです。 改善後の 364095ef は、5/25 のバージョンです。

この表の通り、 改善前の Lua と、改善後 go のトランスコンパイル時間を比べると (/ 25.69 2.22 ) 11.572072072072071 ≒ 1157% 改善しています。

改善後の lua と go の比較では 785%、 改善前と改善後の go の時間を比べると、 263% 改善しています。

今回は以下の高速化を行ないました。

  • meta 情報処理の高速化
  • ast から lua, go へ変換する処理の並列化

以降では、今回の高速化方法ついて説明します。

meta 情報処理の高速化

LuneScript は、モジュールをインポートする際、 そのモジュールが何のクラスや関数を公開いているか?を解析します。 この「何のクラスや関数を公開しているか」の情報が meta 情報です。

この meta 情報を解析するには時間がかかるため、 解析した meta 情報は .meta ファイルとして保存します。 そして、 .meta ファイルがある場合は、そのファイルから meta 情報を取得します。

個々の .lns ファイルを一つずつトランスコンパイルする際は、 この .meta から取得するのが高速化として重要です。

一方で、複数の .lns ファイルを一括してトランスコンパイルする際は、 この方法は多くの無駄な処理が含まれます。

meta 情報の処理

.meta ファイルを生成し、 生成したファイルを読み込んで meta 情報を構築するには、 以下の処理が必要です。

  • .lns ファイルを解析し AST を得る。
  • AST に含まれる .meta 情報から .meta ファイルを出力する。
  • .meta ファイルを読み込み meta 情報を構築する。

ちなみに、この .meta ファイルを読み込み meta 情報を構築する処理を、 ここでは import 処理と言います。

2 つのファイルを処理するケース

例えば、 2 つのファイル(a.lns, b.lns: b.lns から a.lns を import している)を 一括でトランスコンパイルする場合は以下になります。

  • a.lns ファイルを解析し AST_a を得る。
  • AST_a に含まれる meta_a 情報から a.meta ファイルを出力する。
  • b.lns ファイルを解析し AST_b を得る。

    • この解析途中に a.meta を読み込み、 meta_a 情報を構築する。
  • AST_b に含まれる meta_b 情報から b.meta ファイルを出力する。

ここで、 「この解析途中に a.meta を読み込み、 meta_a 情報を構築する。」 は無駄です。

なぜならば、「meta_a 情報」は AST_a に含まれており、 AST_a はメモリ上に残っているため、 「a.meta を読み込み、 meta_a 情報を構築する」ことなく、 メモリ上の AST_a から meta_a を取得できるからです。

import 処理の変更

以下のように import 処理を変更します。

  • a.lns ファイルを解析し AST_a を得る。
  • AST_a に含まれる meta_a 情報から a.meta ファイルを出力する。
  • b.lns ファイルを解析し AST_b を得る。

    • この解析途中に AST_a に含まれる meta_a 情報を取得する。
  • AST_b に含まれる meta_b 情報から b.meta ファイルを出力する。

a.meta を読み込み、 meta_a 情報を構築する。 処理と、 AST_a に含まれる meta_a 情報を取得する。 処理を比較すれば、 圧倒的に後者の方が高速に処理できます。

やっていることは非常に単純ですが、これを実現するのは結構大変でした。。

ast から lua, go へ変換する処理の並列化

トランスコンパイルは、以下の処理行ないます。

  • .lns ファイルを解析して AST を取得する
  • AST から .lua, .go を生成する

これを .lns ファイル分実行します。

例えば a.lns, b.lns, c.lns の 3 つのファイルがあった場合、 次の通り処理します。

  • a.lns ファイルを解析して AST_a を取得する
  • AST_a から .lua, .go を生成する
  • b.lns ファイルを解析して AST_b を取得する
  • AST_b から .lua, .go を生成する
  • c.lns ファイルを解析して AST_c を取得する
  • AST_c から .lua, .go を生成する

ここで、 「AST_a から .lua, .go を生成する」「AST_b から .lua, .go を生成する」「AST_c から .lua, .go を生成する」 の処理は、 基本的には独立して処理できます。

つまり、これら処理は並列して実行可能です。

そこで go rutine を利用して、並列化しています。

しかし、 直感的に 並列化可能 と言っても、 実際に安全に並列化ができるかどうかは別の話です。

シングルスレッドでは問題にならないことも、 マルチスレッドにすると問題になることが良くあります。

今回の 並列化処理 を実現するにあたり、 マルチスレッド化を安全に論理的に実現する方法を、 LuneScript に追加しました。