== null が激重

Posted 2013.04.28 in 吉里吉里

まずは次の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と比較する時は == ではなく === を使おう!


Leave a Reply

*