27. 遅延ロード 編
ある程度の規模のプロジェクトでは、複数のモジュールを import することになります。 そして import したモジュール内でまた別のモジュールを import しています。
スクリプト言語における import は、動的なロード処理を意味します。 あるスクリプトを起動すると、 そのスクリプトが利用している全てのモジュールのロード処理が行なわれた後に、 ようやくスクリプトのメイン処理が動き出します。
例えば、次の図に示すような import の関係があった場合、 メインの mod モジュールの処理が行なわれるのは、 関連する全てのモジュールのロードが終ってから mod モジュールの処理が行なわれます。
digraph G {
rankdir = TB;
mod -> funcA
mod -> funcB
mod -> funcC
funcA -> subA_1
funcA -> subA_2
funcA -> subA_3
funcB -> subB_1
funcB -> subB_2
funcB -> subB_3
funcC -> subC_1
funcC -> subC_2
funcC -> subC_3
}
しかし、 import したモジュールを全て常に使うとは限りません。
例えば、上記の図が 3 つの機能(funcA,funcB,funcC)を提供するプログラムとして考えます。
このプログラムの 3 つの内の一つを選択して実行するような場合、 選択されなかった残りの 2 つの機能は使われません。
import は、モジュールが使われるかどうかに関係なく、 モジュールをロードします。 つまり、実際には使わないモジュールのロード処理に掛る時間や、 ロードしたデータを格納しておくメモリ領域は無駄になることになります。
上記の図で説明すると、 funcA が選択された場合 funcB, subB_1 〜 subB_3、 func_C, subC_1 〜 subC_3 が無駄になります。
プロジェクトの規模が小さい場合、 この無駄を気にする必要はほとんどありません。 しかし、プロジェクトの規模が大きくなった場合、 この無駄を無視できなくなってきます。
例えば一つのモジュールのロード処理に必要な時間が 0.01 秒だったとしても、 100 個のモジュールがあれば 1 秒かかります。
プログラムが常駐するのであれば、ロード処理は一度きりなので、 多少起動時間がかかっても我慢できますが、 常駐せずにリクエスト毎に起動しなければならない場合、 この無駄なロード処理は無視できなくなります。
このロード処理の無駄を無くすのが遅延ロードです。
遅延ロード
遅延ロードは、起動時に関連する全てのモジュールをロードするのではなく、 モジュールが必要になったタイミングでロードする処理方式です。
上記の図で説明すると、 funcA が選択された場合、 アクセスされない funcB, subB_1 〜 subB_3、 func_C, subC_1 〜 subC_3 はロードされません。
これにより、ロード処理の時間、 ロードしたデータを格納しておくメモリ領域を無駄にしません。
遅延ロードの実現方法
遅延ロードは、モジュールにアクセスしたタイミングでロード済みかどうかを判断し、 ロードしていなければロードし、 ロードしていればロード済みのデータを利用します。
一方通常のロードは、 起動時(import したタイミング)に数珠繋ぎで全てのモジュールをロードします。
つまり、遅延ロードでは、通常のロードと比べて判定処理が入るので その分のオーバーヘッドがあります。 もちろんオーバーヘッドは非常に少ないです。 しかし、オーバーヘッドが掛るのは事実であるので、 非常にクリティカルなケースでは性能劣化が発生することも考えられます。
遅延ロードを利用する場合、このようなケースの考慮が必要です。
遅延ロードの影響
前述している通り、遅延ロードには僅かなオーバーヘッドがあります。
しかし、それが影響することはほとんどないでしょう。
それよりも影響する可能性が高いと考えられるのが、実行順序の違いです。
ロード方式の違いによる実行順序の影響
モジュールのトップスコープに書いた処理は、 そのモジュールがロードされた時に実行されます。
例えば次のようなモジュールがあった場合、
// @lnsFront: ok
print( "hoge" );
fn func() {
print( "foo" );
}
このモジュールがロードされたタイミングで hoge が出力されます。
一方で、 func()
はロードされただけでは実行されないので foo は出力されません。
そして、このモジュールを import する次のモジュールがある場合、
// @lnsFront: skip
import Hoge;
print( "bar" );
Hoge.func();
通常ロードの場合、次の出力が行なわれます。
hoge
bar
foo
一方で遅延ロードの場合、次の出力が行なわれます。
bar
hoge
foo
出力結果を比較すると、 bar と hoge の出力順番 が入れ替わっています。
なぜこのようなことが起るかというと、 通常ロードでは
- import したタイミングでサブモジュール Hoge がロードされ、
- そのタイミングで
print( "hoge" )
が実行され、 - サブモジュール Hoge のロード終了後に
print( "bar" )
が実行される。
一方で遅延ロードでは、 通常ロードでは import したタイミングでサブモジュール Hoge がロードされず、
- 最初に
print( "hoge" )
が実行され、 - 次の
Hoge.func()
を実行する直前に、 サブモジュール Hoge がロードされ、 print( "hoge" )
が処理される。
通常ロードと遅延ロードには、このような違いが起る。
ロード方式の違いによる global の影響
LuneScript のアクセス制御には pub/pro/pri/local の他に、global があります。
global の詳細については他の記事を参照していただくとして、 global はモジュールをロードしたタイミングで登録されます。
つまり、遅延ロードの影響で global のデータが登録されるタイミングがズレます。
global を使うケースは少ないと思いますが、注意してください。
global は、既存の Lua コードとの互換を必要するケースにのみの利用を推奨。
使用方法
遅延ロードは次の命令で利用できます。
- import
- module
命令 | ロード処理 |
---|---|
import | コンパイルオプション依存 |
import.l | 遅延ロード |
import.d | 通常ロード |
命令 | ロード処理 |
---|---|
module | コンパイルオプション依存 |
module.l | 遅延ロード |
module.d | 通常ロード |
例えば以下のようにすることで、 Sub モジュールは遅延ロードされます。
// @lnsFront: skip
import.l Sub;
import, module はコンパイルオプション依存です。
コンパイルオプションに次を指定した場合、
--default-lazy
import, module は遅延ロードになります。
なお、import.d, module.d を指定した場合は、
--default-lazy
を指定している時も通常ロードになります。