== null が激重
まずは次のTJSスクリプトを実行してみて欲しい。
function test()
{
var TEST_NUM = 1000000;
var begin, end;
var v = 0;
Debug.message("Test Num: "+TEST_NUM);
begin = System.getTickCount();
for(var i=0; i < TEST_NUM; i++) v == void;
end = System.getTickCount();
Debug.message("v(0) == void : %d ms".sprintf(end-begin));
begin = System.getTickCount();
for(var i=0; i < TEST_NUM; i++) v === void;
end = System.getTickCount();
Debug.message("v(0) === void : %d ms".sprintf(end-begin));
begin = System.getTickCount();
for(var i=0; i < TEST_NUM; i++) v == null;
end = System.getTickCount();
Debug.message("v(0) == null : %d ms".sprintf(end-begin));
begin = System.getTickCount();
for(var i=0; i < TEST_NUM; i++) v === null;
end = System.getTickCount();
Debug.message("v(0) === null : %d ms".sprintf(end-begin));
}
var win = new Window();
win.visible = true;
Debug.console.visible = true;
test();
TJSの特殊値 void, null の比較の速度を計測している。
これを実行すると次のような結果になる。
Test Num : 1000000
v(0) == void : 38 ms
v(0) === void : 33 ms
v(0) == null : 11554 ms
v(0) === null : 38 ms
== null が激重!!!
100万回で 10秒近くということは…
1回あたり0.01msもかかっている!
たいしたことないように思えるかもしれないが
60FPSを実現するには17ms以内に1フレーム中の全ての処理を終えないといけないので
たった100回の比較で1ms食うというのは極めて甚大だ。
一方で、void との比較の場合は null のような問題は起きていない。
なぜこのようなことが起こるのか…
void と null の違い
この問題を追求するには、まず void と null の違いを理解する必要がある。
吉里吉里の値に関して詳しくはこちら。
TJS2 リファレンス > 言語リファレンス > 項
void は値が入ってないことを示す特殊値である。
変数に初期値を指定しない時、変数に最初に入っている値がこの void である。
内部実装では tvtVoid型 の値である。
null は何も指し示していないを示すObject型の値である。
内部実装では tvtObject型 の値である。
void と null は一見似たような性質の値に思える。
しかし
void と null は型が異なる
のである。
実はこの違いが決定的な差を生み出している。
== の実装
吉里吉里のソースコードを追いかけて、原因を探ってみよう。
==演算子が行う比較処理の実装は tTJSVariant::NormalCompare にある。
tjsVariant.h
tjsVariant.cpp
実装を見ると、比較する値の型が同一である場合は、なんら問題なく比較が行われている。
問題なのは、比較する値の型が同一でない場合だ。
型が異なる値の比較
型が異なる値を比較する場合、どんな処理が行われるのか。
その実装も tTJSVariant::NormalCompare にある。
型が異なる場合は…
値を実数に変換して比較する
ただしtvtVoid型とtvtString型については特別な実装が施されている。
tvtVoid型を他の型の値を比較する場合、
相手が数値または文字列であれば数値に変換し、voidを0とみなして比較が行われる。
それ以外の型と比較する場合は、常にfalseとなる。
tvtString型を他の型の値を比較する場合、
他方を文字列に変換して比較が行われる。
実数化の中身
実数化の処理はAsRealに記述されている。
tvtObject型の値がどう変換されるのか見ると…
TJS_CONST_METHOD_DEF(tTVReal, AsReal, ())
{
TJSSetFPUE();
switch(vt)
{
case tvtVoid: return 0;
case tvtObject: TJSThrowVariantConvertError(*this, tvtReal);
case tvtString: return String->ToReal();
case tvtInteger: return (tTVReal)Integer;
case tvtReal: return Real;
case tvtOctet: TJSThrowVariantConvertError(*this, tvtReal);
}
return 0.0f;
}
例外が投げられている!
コレダー!!!
比較の度に例外が投げられていたのでは…
そりゃあー重いはずである。
AsReal から投げられた例外は tTJSVariant::NormalCompare でキャッチされ、falseを返す。
本当の問題
原因は判明したが、よくよく考えるとこの問題は == null に限った問題ではない。
nullでなくても、Object型の値とInt型などの値を比較すれば発生しうる問題だからだ。
またOctetの場合も同様の問題が発生しうることがわかる。
そもそも null と数値を比較するなんてこと自体がおかしいと思うかもしれない。
しかし「数値とオブジェクトの両方が入りうる利便性の高い変数」を作った場合や
「値は設定されているが、不定であることを表す値」として
null を流用しようとした場合などにありえる話である。
対策
問題の原因を理解していれば、この問題を回避するのは簡単だ。
===を使えばいい
===演算子はより厳密な比較を行う比較演算子であり、
異なる型同士の比較の場合は常にfalseを返し、変換は行わない。
ただしObject型同士の比較は == と === とで作用が異なっている点に注意が必要だ。
===演算子はそのオブジェクトのコンテキスト(this)まで比較する。
まとめ
nullと比較する時は == ではなく === を使おう!