bootsnap(1.2.0)のコードリーディングをしました。今回はload_iseqによるrequire, loadの高速化を見ていきます。

まず最初にBootsnap.setupが呼ばれます。load_iseqが関連するところはBootsnap::CompileCache.setupになります。

Bootsnap::CompileCache.setupの処理を見ていきます。setupはBootsnap::CompileCache::ISeq.install!を呼び出します。

install!はRubyVM::InstructionSequenceの特異クラスにInstructionSequenceMixinをprependします。InstructionSequenceMixinはload_iseqが定義されています。RubyVM::InstructionSequence.load_iseqが定義されている場合はrequireやloadの呼び出しでload_iseqが呼ばれます。このフックポイントでInstructionSequenceのバイナリファイルを読み出しています。Bootsnap::CompileCache::Native.fetchの戻り値はInstructionSequenceのインスタンスになります。

Bootsnap::CompileCache::Native.fetchはC拡張のInit_bootsnap()で定義されています。fetchメソッドはbs_rb_fetch()になります。

bs_rb_fetch()は以下のように定義されており、bs_fetch()を呼び出しています。bs_cache_pathではrequireで指定したパスを使ってキャッシュ用のパス(tmp/cache/bootsnap-compile-cache/xx/xxxxx)をcache_path変数にセットします。

bs_fetch()はメインの処理をしている関数になります。長いので抜粋しつつ解説していきます。

pathはrequireで指定した絶対パスが入ります。cache_pathは上述の通りキャッシュ用のファイルパスです。まずrequireで指定されたファイルのファイルディスクリプタをopen_current_file()によって取得します。その際、current_keyにstat情報等をセットします。このキーは主にキャッシュの比較用に使われます。

open_current_file()はstat情報などを抜き出してcurrent_keyにセットします。ファイルが存在すればファイルディスクリプタを返します。

bs_fetch()ではさらにopen_cache_file()によってキャッシュファイルを開き、キャッシュが存在すればcache_key_equal()でキャッシュが有効かどうか(bootsnapのバージョンやファイルのmtime、データサイズを比較)をチェックし、有効であればそのキャッシュファイルを読み込みます。

fetch_cached_data()ではキャッシュファイルを読み込み、bs_storage_to_output()でバイナリからInstructionSequenceに変換します。

bs_storage_to_output()はprot_storage_to_output()経由でBootsnap::CompileCache::ISeq.storage_to_outputを呼び出します。

Bootsnap::CompileCache::ISeq.storage_to_outputはバイナリの文字列からInstructionSequenceに変換します。

bs_fetch()でキャッシュヒットしなかった場合は、bs_read_contents()でrequireしたファイルを読み込み、ファイルの中身をrb_str_new_static()でString化します。

rb_str_new_static()は静的領域にStringのデータを生成するためGCの影響を受けず、さらに文字列をString化するときに不要なメモリ確保をしていないらしいです。

参考 → ruby-trunk-changes r47625 – r47644 – PB memo

String化したデータはbs_input_to_storage()によってキャッシュファイルに格納されます。

bs_input_to_storage()はprot_input_to_storage()、try_input_to_storage()経由でBootsnap::CompileCache::ISeq.input_to_storageを呼び出します。

input_to_storageメソッドは対象のファイルをコンパイルしてInstructionSequenceにしたものをbinary化します(これよく見たら第一引数が _になっていて、読み込んだファイル使っていない…)

その後、atomic_write_cache_file()でファイルに書き出し、前述のbs_storage_to_output()で書き出したファイルからInstructionSequenceを取り出します。

goto succeedではCLEANUPしてInstructionSequenceを返しています。

このようにして、iseqのキャッシュファイルを読み込むことでファイルの読み込みを高速にしています。