Archive for 3月, 2013

ボタンを作る

さてさてここまでFoooの多様な機能を紹介してきたが
ここまで読んだ方々は皆、薄々…疑問に…思っているのではないかと思う。

「いったいどこがアドベンチャーゲーム用の
ゲームエンジンなんだ!」

と。

どんな演出でも実現できるようにと、自由度を追求し続けた結果がコレである。
もはやアドベンチャーゲーム用のエンジンというよりは
より汎用的なマルチメディアエンジンのようなかんじである。
しかしどんなに複雑なことができるとは言っても
Foooはあくまで『動く漫画』を実現するために開発されたゲームエンジンだ。

けれどもゲームを構成する要素は『動く漫画』だけではない。
ゲームがゲームである以上、ユーザーが操作できる部分…
ユーザーインターフェース(UI)がある。

開発が進むにつれ、このUI部分についても『動く漫画』部分と同様に
高度な演出を行いたいという要望が出始めた。

高い汎用性を持ち始めたFooo…
高度な演出が必要なUI…
………………

「UIもFoooスクリプトで作っちゃえばよくね?」

となるのは必然的な流れだった。

Decor_Button ボタンデーカー

そんなこんなでFoooにはUIを実現するデーカーがいろいろあるが、
そのもっとも代表的なものが『ボタンデーカー』だ。
ボタンデーカーは名前の通り、ボタン機能を実現するデーカーである。
ボタンデーカーを作るにはCreateButton命令を使用する。


  CreateButton(name="ボタン", class=@ボタンクラス());

一見するとCreateObject命令となんら違いがない。
ボタンデーカーはオブジェクトデーカーのようにクラスを指定して使う。
ボタンデーカーがオブジェクトデーカーと異なるのは、
ユーザーの操作に応じて予め決められたメソッドを自動的に呼び出す点だ。
次のようなメソッドが呼び出される。

OnButton_Focus ボタンがフォーカスされた時
OnButton_UnFocus ボタンからフォーカスがはずれた時
OnButton_Down ボタンが押し下げられた時
OnButton_Up ボタンが押し上げられた時
OnButton_Enable ボタンが有効になった時
OnButton_Disable ボタンが無効になった時

このような特定のタイミングで自動的に呼び出されるメソッドのことを『イベントハンドラ』と呼ぶ。
ボタンデーカーがやってくれることはこのイベントハンドラの呼び出しだけである。
ボタンに必要な画像の作成や、ボタンの画像の制御などは
コンストラクタやイベントハンドラに動作を書き込むことで実現する。

これは実際の使い方を見たほうがわかりやすいだろう。

Button_Normal Button_Focus Button_Down
ボタン_通常.png ボタン_フォーカス.png ボタン_ダウン.png

style ボタン書式
{
  face="MS ゴシック", size=40, color=white, interval=0
}

class ボタンクラス
{
  method ボタンクラス(string $text)
  {
    CreateImage(name="通常"
      , ox="Center", oy="Middle"
      , image="ボタン_通常.png", sampling="BieLinear");
    CreateImage(name="フォーカス"
      , ox="Center", oy="Middle"
      , image="ボタン_フォーカス.png", sampling="BieLinear"
      , alpha=0%);
    CreateImage(name="ダウン"
      , ox="Center", oy="Middle"
      , image="ボタン_ダウン.png", sampling="BieLinear"
      , alpha=0%);

    CreateText(name="テキスト"
      , ox="Center", oy="Middle"
      , style="ボタン書式", text=$text, sampling="BieLinear");
  }
  method OnEnter()
  {
    Enter(to="*");
  }

  method OnButton_Focus()
  {
    Opaque(to="フォーカス", alpha=100%, time=200);
  }
  method OnButton_UnFocus()
  {
    Opaque(to="フォーカス", alpha=0%, time=200);
  }
  method OnButton_Down()
  {
    Opaque(to="ダウン", alpha=100%, time=0);
    Zoom(to=".", sx=90%, sy=90%, time=50);
  }
  method OnButton_Up()
  {
    Opaque(to="ダウン", alpha=0%, time=200);
    Zoom(to=".", sx=100%, sy=100%, time=200);
  }
}

method Main()
{
  CreateButton(name="ボタン1"
    , x=320, y="Middle"
    , class=@ボタンクラス(text="ボタン1"));
  CreateButton(name="ボタン2"
    , x=640, y="Middle"
    , class=@ボタンクラス(text="ボタン2"));
  CreateButton(name="ボタン3"
    , x=960, y="Middle"
    , class=@ボタンクラス(text="ボタン3"));
  Enter(to="*");
}
YouTube Preview Image

OnButton~メソッドが自動的に呼び出されている様子がわかるだろう。
ボタンデーカーの実装はちょっと手間だが、その分ボタンの動作を細かくカスタマイズできる。


onPaintが二度呼ばれる実験

Posted 2013.03.07 in 吉里吉里

前回の「onPaintが二度呼ばれる問題」の記事において、
現象を確認するスクリプトを掲載し忘れていたのでこちらに掲載しておく。
とても簡単なスクリプトで現象を確認できる。


class TestLayer extends Layer
{
  var timer;
  var paintCount = 0;

  function TestLayer(window, parent)
  {
    super.Layer(...);
    timer = new Timer(onTimer, "");
    timer.interval = 1000;
    timer.enabled = true;
  }

  function finalize()
  {
    invalidate timer;
    super.finalize();
  }

  function onTimer()
  {
    Debug.message("onTimer");
    paintCount = 0;
    update();
  }

  function onPaint()
  {
    Debug.message("onPaint "+(++paintCount));
    update();
//  callOnPaint = false;
  }
}

class TestWindow extends Window
{
  function TestWindow()
  {
    super.Window(); 
    add(new TestLayer(this, null));
    visible = true;
  }
}

var window = new TestWindow();
Debug.console.visible = true;

タイマーで1秒ごとにonTimerを呼び出し、そこからupdate関数を呼び出している。
これを実行するとコンソールに…


onTimer
onPaint 1
onPaint 2
onTimer
onPaint 1
onPaint 2

と表示され続けるはずだ。
1度のonTimerの呼び出しに対してonPaintが二度呼ばれているのがわかる。
そしてcallOnPaint=false;のコメントアウトをはずして再度実行すると…


onTimer
onPaint 1
onTimer
onPaint 1

となる。
二度目のonPaintの呼び出しが抑制されている。

この動作は決して誤った動作ではない。
update関数の作用を正しく理解すると、むしろ至極当たり前の動作だと思えるだろう。
本当の問題はupdate関数を誤って使ってしまいがちなところにあるのである。


onPaintが二度呼ばれる問題

Posted 2013.03.06 in 吉里吉里

画面の再描画を効率化しようと思う時、
誰しもがまず思いつくであろうことがLayerクラスのonPaintイベントの利用だ。
データが変更された時にすぐに再描画を行わずupdate関数を呼び出しておいて
画面の描画前に呼ばれるonPaintでまとめて遅延描画する仕組みである。

シンプルな仕組みのように思えるが…
実はそこには大きな落とし穴があった。

onPaintの不可思議な動作

onPaintを使って再描画を効率化した際、逆に再描画が重くなるという現象に遭遇した。
なぜなのか詳しく調べてみると、意外な事実が判明した。

onPaintが連続して二度呼ばれている!

さらに詳しく調べると、最初に実行されたonPaintを抜ける時に
onPaintを呼び出すことを意味するcallOnPaintフラグがtrueになっていることがわかった。
これが原因っぽい気がするが…吉里吉里のリファレンスには

吉里吉里リファレンス Layerクラス callOnPaint
「onPaint イベント が処理し終わるとこのプロパティは自動的に偽に戻されます」

とあるので、onPaintを抜ける時にtrueになっていても問題ではなさそうだ。
しかし念のためにソースコードを見てみると…
………onPaintを呼び出す前にcallOnPaintフラグを落としている!
リファレンスの誤りだろうか。
途中で仕様変更されたのだろうか。

ともかく、やはりonPaintを抜ける時にcallOnPaintフラグがtrueになっているのが問題なかんじがする。
むしろonPaintを呼び出す前にcallOnPaintフラグが落とされるという事実と照らし合わせるとこれは
onPaint内の処理でcallOnPaintフラグを立てるような処理がなされていることを意味する。
しかしonPaint内でupdateを呼び出してはいないし、
callOnPaintフラグを直接いじるようなこともしていない…
ん、まてよ、updateといえば…

描画プラグインで無効領域を報告するためにupdateを呼び出してる!

そう、プラグインで拡張された描画関数を呼び出していたのである。
その中のupdate呼び出しがcallOnPaintフラグをtrueにしている真犯人だったのだ。

さてさてonPaintを抜ける時にcallOnPaintがtrueになる原因は判明したが、
それでなぜonPaintが二度呼ばれることになるのか。
それを理解するにはまず吉里吉里の再描画の仕組みと、
update関数の作用について知る必要がある。

画面の再描画の流れ

吉里吉里はイベントシステムである。
吉里吉里 イベントシステム

画面の再描画もイベントによって管理されている。
画面の再描画は描画イベント(tTVPWinUpdateEvent)が実行された時に行われる。

描画イベントは原則的になんらかの描画関数を呼び出した時に発行される。
また描画関数は描画した領域を無効領域として報告する。
発行された描画イベントはイベントキューに蓄えられる。
イベントキューに蓄えられる描画イベントは原則として各画面につき1個で、
重複するイベントは無視される。
そして描画イベントはイベント配信処理において低優先度で実行される。

描画イベントは実行されるとまず
callOnPaintフラグがtrueな全てのレイヤーに対してonPaintの呼び出しを行う。
次に全てのレイヤーの無効領域の範囲のバッファの更新を行う。
そしてバッファの最終的な合成結果を画面に転送する。

update関数の作用

update関数には3つの作用がある。

1.callOnPaintフラグをtrueにする
2.無効領域を報告する
3.描画イベント(tTVPWinUpdateEvent)を発行する

まず1によって次回の画面の再描画時にonPaintを呼ぶことを予約する。
ここで重要な事実がある。
callOnPaintフラグはあくまでonPaintを呼ぶ予約にすぎないということだ。
callOnPaintフラグを立てただけでは、画面の再描画は行われない。
画面の再描画が行われるのは、描画イベントが発行され実行された時である。

つまりレイヤーの再描画を行わせる目的で
layer.callOnPaint = true;
などとやっても期待するような動作にはならない。
update関数はcallOnPaintフラグをtrueにするのと同時に描画イベントを発行するので、
update関数を呼び出せば期待通り画面の再描画が行われる。

onPaintが二度呼ばれる理由

ここまでわかればonPaintが二度呼ばれる理由はすぐわかる。
onPaint関数内で、update関数を呼び出したらどうなるか。
callOnPaintフラグがtrueになり、描画イベントが発行される。
描画イベントが発行されるということは…画面の再描画が行われる!
そしてcallOnPaintフラグがtrueなので、再びonPaintが呼び出される!

しかしここでまた不思議なことがある。
そのような流れで再びonPaintが呼ばれるのであれば、
onPaintが無限に呼び出され続けることになりそうなものである。
けれどもそうはならない。

これにはイベントキューの処理上の仕様が関係している。
描画イベントは原則としてイベントキューに各画面につき1個しか蓄えられないのだが、
描画イベント実行中は例外的に2個までイベントキューに蓄えられるのである。
つまり現在実行中の描画イベントに加えて、もう1個イベントキューに蓄えられるということである。
これは言い換えれば、画面の再描画が行われるのは
一度のイベント配信処理において最大2回までということである。
よって無限に呼び出されるようなことにはならない。

update関数を呼ばなくても二度目の再描画は行われる

描画イベントは標準の描画関数を呼び出した時にも発行されるので
実はonPaintで標準の描画関数を使った際にも二度目の画面の再描画処理は行われる。

けれどもこれはほとんどの場合問題にならない。
なぜならonPaintで発生した無効領域はその後のバッファの更新で処理されるからだ。
バッファの更新を終えるとそこまでに蓄えられていた無効領域はクリアされる。
二度目の画面の再描画時には無効領域は空になっているので
実際には具体的な再描画はなんら行われずすぐに処理を終える。

しかし1度目のonPaintでupdateを呼び出して2度目のonPaintで描画を行った場合は
再び無効領域が発生し画面の再描画が再度実施されてしまう。
これは非常に無駄な処理になりかねない。

プラグインでupdateを使うのは誤り?

プラグインで独自の描画関数を追加する際には、吉里吉里の標準の描画関数と同じように
描画した領域を無効領域として報告するようにするのが正しい実装である。
もしそうしなければ、レイヤーのバッファの更新が正しく行われない可能性がある。
たまたま吉里吉里の標準の描画関数を一緒に呼び出していて
うまく動作する場合もあるが…たまたまにすぎない。

プラグインからの無効領域の報告にはupdate関数が一般的に使用されているようだ。
しかし無効領域の発生を報告するためだけを目的として
update関数を使用するのはおそらく本来誤りだ。

なぜなら吉里吉里の標準の描画関数は、update関数の2と3の処理しか行わないからである。
つまり1の動作…callOnPaintフラグを立てる処理が余計なのである。
そもそもupdate関数は本来onPaintとセットで使うべきものなのだろう。

しかし無効領域の報告だけを目的としてupdate関数を使ったプラグインが既に多くある。
その現状を踏まえた上で、この問題を回避する方法について考えてみよう。

対策1: onPaintで明示的にcallOnPaintフラグを落とすようにする

ズバリ言って、updateによってcallOnPaintがtrueにされてしまうのであれば
明示的にcallOnPaintフラグを落としてしまえばいい。
ここまでごちゃごちゃと理屈を述べてきたが、実際これだけでおおかた問題を回避できる。


function onPaint()
{
  // なんらかの描画処理

  callOnPaint = false;
}

対策2: onPaintで無駄な再描画をしないようにしておく

再描画フラグなどを独自に設けて、無駄な再描画をしないようにしておくのも良い方法だ。
何かが何かして二度目のonPaintが実行されたとしても、これならば無駄な再描画を確実に回避できる。


function onPaint()
{
  if(!isNeedRedraw) return;
  isNeedRedraw = false;

  // なんらかの描画処理
}

対策3: updateを呼ぶ際にcallOnPaintを維持するようにする

プラグイン側で無効領域の発生を報告するためだけにupdate関数を使うのは誤り!
とは言ってもそのようなことを直接できる関数はupdate関数しかないので
現実問題としてupdate関数を使わざるを得ない。

そこで、意図しない不必要なonPaintの呼び出しを避けるために
callOnPaintフラグを一時保存してupdateを呼び出した後に書き戻すようにする。
たとえば次のような関数を作って使うようにする。


static tjs_uint32 g_Hint_callOnPaint = 0;
static tjs_uint32 g_Hint_update = 0;

bool Update(iTJSDispatch2* pLayer, const RECT* pRect)
{
  // レイヤークラス取得
  tTJSVariant Value;
  TVPExecuteExpression(TJS_W("Layer"), &Value);
  iTJSDispatch2 *pLayerClass = Value.AsObjectNoAddRef();

  // パラメータを用意
  const int PARAM_NUM = 4;
  tTJSVariant Params[PARAM_NUM];
  Params[0] = pRect->left;
  Params[1] = pRect->top;
  Params[2] = pRect->right - pRect->left;
  Params[3] = pRect->bottom - pRect->top;

  // パラメータポインタの配列を用意
  tTJSVariant* ParamPtrs[PARAM_NUM];
  for(int i=0; i < PARAM_NUM; i++) ParamPtrs[i] = &Params[i];

  // callOnPaintを退避
  tTJSVariant callOnPaint;
  if(TJS_FAILED(pLayerClass->PropGet(0, TJS_W("callOnPaint"), &g_Hint_callOnPaint, &callOnPaint, pLayer))) return false;

  // 領域更新
  tTJSVariant result;
  if(TJS_FAILED(pLayerClass->FuncCall(0, TJS_W("update"), &g_Hint_update, &result, PARAM_NUM, ParamPtrs, pLayer))) return false;

  // callOnPaintを復元
  if(TJS_FAILED(pLayerClass->PropSet(0, TJS_W("callOnPaint"), &g_Hint_callOnPaint, &callOnPaint, pLayer))) return false;

  return true;
}

まとめ

onPaint関数はさらにpiledCopy関数の呼び出し時や
トランジッション時にも呼ばれる可能性があるため問題はいっそう複雑だ。
update関数の使い方には細心の注意を心がけたい。


Dictionaryの具体的修正点

Posted 2013.03.05 in 吉里吉里

吉里吉里(TJS2)のDictionary(辞書配列)の作成に関する高速化策について
試しにいじってみた内容を具体的にメモがてら書いておく。
コードの黄色文字が追加修正部分。

吉里吉里公式のソースコードはこちら。
https://sv.kikyou.info/trac/kirikiri/

吉里吉里のソースコードのビルドには古いBorland C++ Builderが必要。
kirikiri2\src\core\environ\win32 に
5, 6, 2006, 2007のプロジェクトファイルがあるのでそれを使う。
6を使ったが若干修正が必要だった。いじろうとする人はまずここで躓くかも。

ハッシュテーブルに再構築用の関数を追加

ハッシュテーブルの実装を担っているのはtTJSCustomObjectクラスである。
現在の要素数から適切なハッシュテーブルサイズを計算して
ハッシュテーブルの再構築を行うRebuildHash関数が既にあるので
これを分解してハッシュテーブルを外から細かく操作する関数を追加する。


tjsObject.h
class tTJSCustomObject : public tTJSDispatch { // 略 public: // ハッシュテーブル操作 tjs_int CalcHashSize(tjs_int count); void RebuildHashByCount(tjs_int count); void RebuildHashBySize(tjs_int size); }

tjsObject.cpp
// 要素数から適切なハッシュテーブルサイズを計算する tjs_int tTJSCustomObject::CalcHashSize(tjs_int count) { // decide new hash table size tjs_int r, v = count; // 略(ReubildHash関数からコピペ) return newhashsize; } // ハッシュテーブルを要素数指定で再構築する void tTJSCustomObject::RebuildHashByCount(tjs_int count) { RebuildHashBySize(CalcHashSize(count)); } // ハッシュテーブルをサイズ指定で再構築する void tTJSCustomObject::RebuildHashBySize(tjs_int size) { // rebuild hash table RebuildHashMagic = TJSGlobalRebuildHashMagic; tjs_int newhashsize = size; if(newhashsize == HashSize) return; // 略(ReubildHash関数からコピペ) }

辞書に再構築用の関数を追加

Dictionaryの実装はtTJSDictionaryNIクラスにあるのでそこにもリビルド用の関数を追加する。
tTJSCustomObjectクラスのオブジェクトを内包しているので単に処理を受け渡すだけである。
またリビルドが適切かどうか判断するために要素数とハッシュサイズを取得する関数も追加しておく。


tjsDictionary.h
class tTJSDictionaryNI : public tTJSNativeInstance, public tTJSSaveStructuredDataCallback { // 略 public: // ハッシュテーブル操作 void RebuildHashByCount(tjs_int count); void RebuildHashBySize(tjs_int size); tjs_int GetHashSize() const; tjs_int GetCount() const; }

tjsDictionary.cpp
// ハッシュテーブルを要素数指定で再構築する void tTJSDictionaryNI::RebuildHashByCount(tjs_int count) { Owner->RebuildHashByCount(count); } // ハッシュテーブルをサイズ指定で再構築する void tTJSDictionaryNI::RebuildHashBySize(tjs_int size) { Owner->RebuildHashBySize(size); } // ハッシュテーブルサイズを取得 tjs_int tTJSDictionaryNI::GetHashSize() const { return Owner->HashSize; } // 要素数を取得 tjs_int tTJSDictionaryNI::GetCount() const { return Owner->Count; }

rebuild関数を追加する

さてこれで準備が整った。
まず最も簡単な作業から。
吉里吉里のDictionaryクラスにrebuild関数を追加する。


tjsDictionary.cpp
TJS_BEGIN_NATIVE_METHOD_DECL(/*func.name*/rebuild) { TJS_GET_NATIVE_INSTANCE(/* var. name */ni, /* var. type */tTJSDictionaryNI); if(!ni->IsValid()) return TJS_E_INVALIDOBJECT; tjs_uint32 count = 0; if(numparams >= 1 && param[0]->Type() != tvtVoid) count = (tjs_int)*param[0]; ni->RebuildHashByCount(count); return TJS_S_OK; } TJS_END_NATIVE_STATIC_METHOD_DECL(/*func.name*/rebuild)

アサイン時に予め再構築する

アサインの修正も簡単だ。
Dictionaryのassign関数と、assignStruct関数の具体的な処理は
Assign, AssignStructure関数にあるのでそこをちょこっとだけいじる。


tjsDictionary.cpp
void tTJSDictionaryNI::Assign(iTJSDispatch2 * dsp, bool clear) { // copy members from "dsp" to "Owner" // determin dsp's object type tTJSArrayNI *arrayni = NULL; tTJSDictionaryNI *dicni = NULL; if(dsp && TJS_SUCCEEDED(dsp->NativeInstanceSupport(TJS_NIS_GETINSTANCE, TJSGetArrayClassID(), (iTJSNativeInstance**)&arrayni)) ) { // convert from array if(clear) Owner->Clear(); // 予め要素数指定で再構築する tjs_int count = Owner->Count + arrayni->Items.size()/2; if(64 <= count) // 足きり Owner->RebuildHashByCount(count); tTJSArrayNI::tArrayItemIterator i; for(i = arrayni->Items.begin(); i != arrayni->Items.end(); i++) { tTJSVariantString *name = i->AsStringNoAddRef(); i++; if(arrayni->Items.end() == i) break; Owner->PropSetByVS(TJS_MEMBERENSURE|TJS_IGNOREPROP, name, &(*i), Owner); } } else if(dsp && TJS_SUCCEEDED(dsp->NativeInstanceSupport(TJS_NIS_GETINSTANCE, TJSGetDictionaryClassID(), (iTJSNativeInstance**)&dicni)) ) { // dictionary if(clear) Owner->Clear(); tAssignCallback callback; callback.Owner = Owner; // 予め要素数指定で再構築する tjs_int count = Owner->Count + dicni->GetCount(); if(64 <= count) // 足きり Owner->RebuildHashByCount(count); dsp->EnumMembers(TJS_IGNOREPROP, &tTJSVariantClosure(&callback, NULL), dsp); } else { // otherwise if(clear) Owner->Clear(); tAssignCallback callback; callback.Owner = Owner; dsp->EnumMembers(TJS_IGNOREPROP, &tTJSVariantClosure(&callback, NULL), dsp); } } void tTJSDictionaryNI::AssignStructure(iTJSDispatch2 * dsp, std::vector &stack) { // assign structured data from dsp tTJSDictionaryNI *dicni = NULL; // tTJSArrayNI *dicni = NULL; // 元々これだったけどミス? if(TJS_SUCCEEDED(dsp->NativeInstanceSupport(TJS_NIS_GETINSTANCE, ClassID_Dictionary, (iTJSNativeInstance**)&dicni)) ) { // copy from dictionary stack.push_back(dsp); try { Owner->Clear(); // 予め要素数指定で再構築する tjs_int count = dicni->GetCount(); if(64 <= count) // 足きり Owner->RebuildHashByCount(count); tAssignStructCallback callback; callback.Dest = Owner; callback.Stack = &stack; dsp->EnumMembers(TJS_IGNOREPROP, &tTJSVariantClosure(&callback, NULL), dsp); } catch(...) { stack.pop_back(); throw; } stack.pop_back(); } else { TJS_eTJSError(TJSSpecifyDicOrArray); } } void tTJSDictionaryNI::AssignStructure

ロード時に予め再構築する

ロード時の処理は(const)がつく場合とつかない場合とで処理が異なるが
とりあえず(const)がつく場合だけ修正。

この修正は少々手間だ。
データ文字列の解析時に構文解析と意味解析を同時に行っているため、
要素を実際に追加する前に要素の総数がわからないからである。
そこで要素をすぐに辞書に追加せずにいったんリストに蓄え、
要素の総数が判明したら一気に要素を追加するようにする。

まず、式ノードに追加要素を一時保管する処理を追加する。


tjsInterCodeGen.h
class tTJSExprNode { // 略 protected: // 追加要素一時保管処理 struct SElementData { tTJSString name; tTJSVariant value; }; list< SElementData* > *pElementTempList; public: void AddDictionaryElementExBegin(); void AddDictionaryElementEx(const tTJSString & name, const tTJSVariant & val); void AddDictionaryElementExEnd(); }

tjsInterCodeGen.cpp
#include "tjsObject.h" #include "tjsDictionary.h" tTJSExprNode::tTJSExprNode() { // 略 pElementTempList = NULL; } // 辞書に要素追加開始 void tTJSExprNode::AddDictionaryElementExBegin() { pElementTempList = new list< SElementData* >; } // 辞書に要素追加 void tTJSExprNode::AddDictionaryElementEx(const tTJSString & name, const tTJSVariant & val) { SElementData* pData = new SElementData; pData->name = name; pData->value = val; pElementTempList->push_back(pData); } // 辞書に要素追加終了 void tTJSExprNode::AddDictionaryElementExEnd() { tjs_uint32 Size = pElementTempList->size(); if(Size == 0) { delete pElementTempList; pElementTempList = NULL; return; } // 予め要素数指定で再構築する if(64 <= Size) // 足きり { tTJSDictionaryNI *dicni = NULL; tTJSVariantClosure clo = Val->AsObjectClosureNoAddRef(); if(TJS_SUCCEEDED(clo.Object->NativeInstanceSupport(TJS_NIS_GETINSTANCE, TJSGetDictionaryClassID(), (iTJSNativeInstance**)&dicni)) ) { dicni->RebuildHashByCount(Size); } } // 辞書に蓄えておいた要素を流し込む list< SElementData* >::iterator it = pElementTempList->begin(); list< SElementData* >::iterator end = pElementTempList->end(); while(it != end) { SElementData* pElement = *it; AddDictionaryElement(pElement->name, pElement->value); delete pElement; it++; } pElementTempList->clear(); delete pElementTempList; pElementTempList = NULL; }

次に構文解析処理で一時保管用関数を使用するように修正。
AddDictionaryElementをAddDictionaryElementExに置き換える。

※追記
以下のtjs.tab.cppはtjs.yから生成されるファイルで、
正式にはそちらのファイルを修正してbisonを通して生成すべきのようです。
じんさんご指摘ありがとうございます。


tjs.tab.cpp
/* Line 1455 of yacc.c */ #line 839 "syntax/tjs.y" { tTJSExprNode *node = cc->MakeNP0(T_CONSTVAL); iTJSDispatch2 * dsp = TJSCreateDictionaryObject(); node->SetValue(tTJSVariant(dsp, dsp)); dsp->Release(); cc->PushCurrentNode(node); cn->AddDictionaryElementExBegin(); ;} break; case 255: /* Line 1455 of yacc.c */ #line 846 "syntax/tjs.y" { (yyval.np) = cn; cn->AddDictionaryElementExEnd(); cc->PopCurrentNode(); ;} break; case 259: /* Line 1455 of yacc.c */ #line 859 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (4)].num)), - lx->GetValue((yyvsp[(4) - (4)].num))); ;} break; case 260: /* Line 1455 of yacc.c */ #line 860 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (4)].num)), + lx->GetValue((yyvsp[(4) - (4)].num))); ;} break; case 261: /* Line 1455 of yacc.c */ #line 861 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (3)].num)), lx->GetValue((yyvsp[(3) - (3)].num))); ;} break; case 262: /* Line 1455 of yacc.c */ #line 862 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (3)].num)), tTJSVariant()); ;} break; case 263: /* Line 1455 of yacc.c */ #line 863 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (3)].num)), (yyvsp[(3) - (3)].np)->GetValue()); ;} break; case 264: /* Line 1455 of yacc.c */ #line 864 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (3)].num)), (yyvsp[(3) - (3)].np)->GetValue()); ;} break;

まとめ

足きりの定数は適当に決めたものである。
pElementTempListは例外が飛んだ場合などに備えてクリア処理やデストラクタでも破棄すべきだろう。
辞書生成直後に再構築するようなケースではコンストラクタを通した方がより無駄がないと思うが
今回はできるだけ元の構造をいじらないアプローチにしてみた。


マスクを動かす

マスクデーカーはマスク画像を使って表示制限を行うが、
「画像」とは「画像ファイル」じゃなくてもいいんじゃないか?
と、勘のいい人なら気づくかもしれない。

そう、マスクデーカーにはマスクとして「画像ファイル」ではなく、
「画像として扱えるデーカー」を指定することもできる。
やり方はGetDecor関数でデーカーへの参照を取得しmaskパラメータに渡すだけだ。


CreateMask(name="マスク", mask=GetDecor("画像を持つデーカー"));

これでいったい何ができるのかというと、
マスクにバッファデーカーを指定することでマスク画像を動かすことが可能になる。
試してみよう。


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

	int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Test(string $effect)
{
  Transform(to="マスク", effect=$effect, time=1000);
  wait 1000;
  Transform(to="マスク", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateBuffer(name="バッファ", w=1280, h=720, x=10000);
  CreateImage(name="バッファ/星", image="マスク.png"
    , x="Center", y="Middle", ox="Center", oy="Middle");
  Enter(to="バッファ");
  Enter(to="バッファ/星");
  Rotate(to="バッファ/星", time=30000, angle=360);
  Zoom(to="バッファ/星", time=30000, sx=400%, sy=400%);

  CreateImage(name="画像2", image="画像2.png");
  CreateMask(name="マスク", mask=GetDecor("バッファ"), mask_mode="Blue");
  CreateImage(name="マスク/画像1", image="画像1.png");
  CreateObject(name="マスク/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="*");
  Enter(to="マスク/*");
  wait 3000;

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}
YouTube Preview Image

mask_modeというパラメータは画像の何の情報を不透明度として扱うかという指定である。
デフォルトでは自動で判断されるのだが、今回は都合が悪いので直接指定している。
“Blue”は青色要素を不透明度とする指定だ。

ちなみに似たようなことがUniversalTransit命令などでも可能である。


マスクを使う

バッファデーカーは子デーカーの表示をバッファの領域内に制限するので
ウィンドウデーカーのような用途でも使用できる。

実はウィンドウデーカーには「回転できない」という欠点がある。
ただしその分非常に高速に動作する。
バッファデーカーはウィンドウデーカーよりもかなり低速だが、
画像として扱えるのでもちろん回転も可能だ。

しかしウィンドウデーカーもバッファデーカーも表示を制限できるのは四角の形だけである。
丸形や星形に表示を制限するといったようなことはできない。

Decor_Mask マスクデーカー

バッファデーカーを拡張し、自由な形に表示制限できる窓を実現するのがマスクデーカーである。
マスクデーカーはマスク画像を使ってバッファに写しこまれた画像に透明部分を設定する。
バッファは画像として扱えるのだから、バッファの表示を制限したい部分を透明にしてやれば、
自由な形に表示を制限することができるという寸法だ。

マスクデーカーを作成するにはCreateMask命令を使用する。


CreateMask(name="マスク", mask="マスク.png");

組み込んでみよう。

マスク.png
Mask


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

	int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Test(string $effect)
{
  Transform(to="マスク", effect=$effect, time=1000);
  wait 1000;
  Transform(to="マスク", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateImage(name="画像2", image="画像2.png");
  CreateMask(name="マスク", mask="マスク.png");
  CreateImage(name="マスク/画像1", image="画像1.png");
  CreateObject(name="マスク/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="*");
  Enter(to="マスク/*");
  wait 3000;

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}
YouTube Preview Image

今回も変更点はごく僅かだ。
このようにFoooではデーカーの「組み合わせ方」を利用した機能がとても多く
Foooの大きな特徴の一つである。


バッファを使う

複数のデーカーを一枚の画像として扱えるようにするには
もう一つのアプローチがある。

スナップショットデーカーの動作サンプルを見ればわかるが、
複数のデーカーを一枚の画像として扱えるようになってはいるものの、
画像は停止してしまっていた。
スナップショットなのだから当然と言えば当然である。

毎フレームスナップショットをとるようにして、
デーカーを動かしながら一枚の画像として扱えるようにできなくもないが
ちょっと非効率なかんじがする。

Decor_Buffer バッファデーカー

そんな時はバッファデーカーを使う。
バッファデーカーは子デーカーの表示を自身が持つバッファに写しこみ、
一枚の画像として扱えるようにするデーカーである。
子デーカーが動くとバッファの内容は自動的に適切に更新される。

バッファデーカーを作るにはCreateBuffer命令を使用する。


CreateBuffer(name="バッファ", w=1280, h=720);

組み込んでみよう。


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

	int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Test(string $effect)
{
  Transform(to="バッファ", effect=$effect, time=1000);
  wait 1000;
  Transform(to="バッファ", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateImage(name="画像2", image="画像2.png");
  CreateBuffer(name="バッファ", w=1280, h=720);
  CreateImage(name="バッファ/画像1", image="画像1.png");
  CreateObject(name="バッファ/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="*");
  Enter(to="バッファ/*");
  wait 3000;

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}
YouTube Preview Image

単に対象をバッファデーカーの子デーカーにしているだけにしか見えないが
これだけでデーカーを動かしたまま一枚の画像として扱えるようになる。


スナップショットをとる

ここまでに紹介したTransit, UniversalTransit, Transformは
デーカーの見た目を変化させる命令であり、描画命令と呼ぶ。
描画命令は画像を表示する機能を持つデーカーに共通して使用できる。
イメージデーカー、テキストデーカー、バルーンデーカーなどだ。

しかしこれらの命令の効果は、その命令を受けたデーカーの見た目だけに限定される。
つまりこれらの命令を使って”複数のデーカーを合体させたもの”を
まとめてトランジッションしたりまとめて変形させるといったようなことはできない。

だがそのようなことがまったく不可能なわけではない。
別の機能と組み合わせることで実現できる。

Decor_Snapshot スナップショットデーカー

スナップショットデーカーはイメージデーカーの特殊版とも言えるデーカーで
他のデーカーの表示を取り込んで自身の画像とする機能を持つデーカーである。
このデーカーを使えば、”複数のデーカーを合体させた画像”を作ることができる。
そしてそのスナップショットデーカーに対してトランジッションを行えば、
“複数のデーカーを合体させたもの”をトランジッションさせるようなことができる。

スナップショットデーカーを作るにはCreateSnapshot命令を使う。


CreateSnapshot(name="スナップ");

そしてスナップショットを取りたい位置でRequest命令で@Snapshot()を指示する。


Request(to="スナップ", order=@Snapshot(target="画像1", w=1280, h=720));

target パラメータにはスナップショットをとりたい対象のデーカー名を指定する。
省略すると画面全体が対象となる。
w パラメータはスナップショット画像の横幅を指定する。
h パラメータはスナップショット画像の縦幅を指定する。
実際に組み込んでみよう。


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

    int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Test(string $effect)
{
  Transform(to="スナップ", effect=$effect, time=1000);
  wait 1000;
  Transform(to="スナップ", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateImage(name="画像2", image="画像2.png");
  CreateImage(name="画像1", image="画像1.png");
  CreateObject(name="画像1/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  CreateSnapshot(name="スナップ");
  Enter(to="*");
  Enter(to="画像1/*");
  wait 3000;

  Request(to="スナップ", order=@Snapshot(target="画像1", w=1280, h=720));
  Exit(to="画像1");

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}
YouTube Preview Image

ずいぶん長いが重要なところは黄色で示されている部分だけだ。
Exit命令はEnter命令とは逆にデーカーを消す命令である。
スナップショットをとったら元の画像は必要ないのですぐ消すようにしている。

実行結果を見ると確かにスナップショットをとって
それを一つのイメージデーカーのように扱えているのがわかるだろう。
スナップショットデーカーは場面転換の演出を作る際などにうってつけである。


Dictionaryを高速化する

Posted 2013.03.01 in 吉里吉里

Dictionaryの作成が遅くなる理由が判明したわけだが、
これはちょっと工夫すれば格段に高速化できる余地がありそうだ。
しかしズバリ言って、現バージョンの吉里吉里ではおそらく解決方法はない。
Dictionaryクラスには内部のハッシュテーブルを操作するような関数が存在しないからだ。
そこで吉里吉里のソースコードをいじる形で、いくつか解決案を考えてみよう。

案1: 検索しないようにする

辞書への要素の追加が重くなっているそもそもの原因は、
同一キーの要素がないか検索を行っている処理にある。
辞書に追加するキーに重複がないと予めわかっている場合は、
この検索処理は完全に無駄な処理だ。

検索処理を省いてしまえば、ハッシュテーブルが小さくても
要素の追加処理の負荷は軽微なものになる。
これは抜本的な解決策のように思える。

例えば要素追加時に検索を行わないようにするDirtyフラグを作り、
絶対に同一キーの要素が追加されないとわかっている時にフラグをONにして要素を追加し、
追加し終わったらフラグをOFFにする…といったような実装が考えられる。

しかし同一キーの要素がないか検索しないということは底知れない危険性を孕んでいる。
人間はミスをする生き物なので、うっかり同一キーの要素を入れてしまったり
何かの手違いでフラグをONにしっぱなしにしてしまう可能性がある。
要素を一気に流し込むassignのような関数内でON/OFFを行うようにすれば
OFFにし忘れるミスは防げるが…人為的ミスが入り込む可能性は依然拭いきれない。

仮に同一キーの要素を複数ハッシュテーブルに追加してしまった場合…
おそらくソレによって生じるバグの原因を突き止めることは相当困難だろう。
検索自体を省く方法は確実に効果が見込めるが、そこには大きなリスクがつきまとう。

案2: 検索を高速化する

検索を省かず、検索処理を見直して高速化する方法はどうだろう。
例えば同一値のリストをただのリストではなく『ソート済みリスト』にする。
ソート済みリストにすればリストの最後まで要素を調べなくても
リストの途中で同一キーの要素がないことを判断することが可能になる。
平均2倍程度の高速化が期待できる。

しかしリストをソート済みリストにすると、
高速化の目的で行われている別のトリックが使えなくなる問題がある。
そのトリックとは、要素が検索された時にその要素をリストの先頭に繰り上げて
次回の検索を最適化する処理だ。
この処理ができなくることとのトレードオフはなんとも難しい。
またこのような挙動変更は互換の点においてもややリスクがある。

案3: ハッシュテーブルを予め大きくしておく

検索が遅くなる原因は、ハッシュテーブルが小さすぎて
同一ハッシュ値の要素のリストがものすごく伸びてしまうことにある。
これは裏を返せば、ハッシュテーブルを予め十分な大きさにしてから
要素を追加すれば問題を回避できるということである。

この方法が効果的なシチュエーションは限定的ではあるが、
検索処理は省かないし、人為的ミスが入り込む余地も少ないし、
挙動も変化しないので、リスクはほとんどないと言っていい。
そして何よりDictionaryの仕組み自体には手を入れないので
ほんの少しの修正で実現可能だ。

ただし追加する要素数が少ない場合は、
ハッシュテーブルの再構築が逆に無駄な処理になるので
ある程度の数で処理を足きりする必要があるだろう。
そのためにも具体的にハッシュテーブルの初期サイズがどのくらいなのか確認しておこう。

吉里吉里のハッシュテーブルの実装はこちら。
tjsObject.h
tjsObject.cpp

初期サイズはtjsObject.hのTJS_NAMESPACE_DEFAULT_HASH_BITSで定義されている。
値は3だ。実際のテーブルサイズは(1<<bits)で計算されるので…
要するに8である。すごく小さい!

そしてこのテーブルサイズにおける適切な要素数は
tjsObject.cppの”decide new hash table size”の部分で行われている計算式に基づけば、
テーブルサイズの半分からその半分の間…つまり2~3である。

ハッシュテーブルのサイズを予め大きくしておくことはかなり効果がありそうだ。
それではこの方向性で具体的な高速化策を考えてみよう。

高速化策1: ハッシュテーブルを再構築する関数を追加する

RebuildHashの仕組みを流用して、ハッシュテーブルを
指定の要素数に適したサイズに再構築するrebuild関数を追加する。
スクリプトで要素を大量に追加する前にこの関数を呼び出すことで
大幅な高速化が期待できる。

高速化策2: ロード時に予め再構築する

辞書に大量の要素を追加する際に極めて重くなるのは、
辞書の読み込み処理においても同じである。
そこで辞書の読み込み処理の部分でも、
ハッシュテーブルを適切なサイズにしてから要素を追加するように改造する。

具体的には辞書に追加する要素をいったんリストに蓄え、
要素数が判明したら再構築を行って、その後で辞書に要素を流し込むようにする。

高速化策3: アサイン時に予め再構築する

配列や辞書のアサイン時にもはやり同様に重くなる。
アサイン時には追加要素数は自明なので、この改造はすごく簡単だ。

実験してみる

で、実際にこの3つの対策を施したexeを作りテストしてみた。
テストスクリプトは以下の通りだ。


class SimpleWindow extends Window
{
  function SimpleWindow()
  {
    super.Window();
    add(new Layer(this, null));
    visible = true;
  }
  function finalize()
  {
    super.finalize();
  }
}

var win = new SimpleWindow();
Debug.console.visible = true;

//////////////////////////////////////////////////////////////////////
// 補助
//////////////////////////////////////////////////////////////////////

// 計測開始/終了
var g_timeBegin;
function testBegin()
{
  System.doCompact(); // メモリ強制開放

  g_timeBegin = System.getTickCount();
}
function testEnd()
{
  return System.getTickCount() - g_timeBegin;
}

//////////////////////////////////////////////////////////////////////
// テスト本体
//////////////////////////////////////////////////////////////////////

var FIND_NUM = 1000; // 検索回数
var FILE_NAME = System.dataPath+"test.txt"; // 一時ファイル名
  
function testMain(CreateNum, testCase, count)
{
  //------------------------------------------------------------------
  // 準備
  //------------------------------------------------------------------

  var CREATE_NUM = CreateNum; // 作成要素数

  // 検索対象をランダムに作成
  // *2 して「ヒットしないもの」も探す
  var findTable = [];
  for (var i = 0; i < FIND_NUM; i++)
  {
    findTable[i] = int (Math.random()*(CREATE_NUM*2-1));
  }

  //------------------------------------------------------------------
  // 配列
  //------------------------------------------------------------------

  // 作成
  var array1 = [];
  testBegin();
  for(var i = 0; i < CREATE_NUM; i++)
  {
    var name = "hash_number_is_" + "%06d".sprintf(i);
    array1[i] = name;
  }
  var timeArrayCreate = testEnd();

  // 保存
  testBegin();
  array1.saveStruct(FILE_NAME);
  var timeArraySave = testEnd();

  // 読み込み
  testBegin();
  var array2 = Scripts.evalStorage(FILE_NAME);
  var timeArrayLoad = testEnd();

  // アサイン実行
  testBegin();
  var array3 = [];
  array3.assign(array1);
  var timeArrayAssignA = testEnd();

  // 検索
  // findは返り値を受けとらないと処理されないことに注意
  testBegin();
  var temp;
  for (var i = 0; i < FIND_NUM; i++)
    temp = array1.find("hash_number_is_" + "%06d".sprintf(findTable[i]));
  var timeArrayFind = testEnd();

  //------------------------------------------------------------------
  // 辞書
  //------------------------------------------------------------------

  // 作成
  var dictionary1 = %[];
  testBegin();
  for(var i = 0; i < CREATE_NUM; i++)
  {
    var name = "hash_number_is_" + "%06d".sprintf(i);
    dictionary1[name] = i;
  }
  var timeDictionaryCreate = testEnd();

  // 作成2(指定数でRebuildHashする)
  var dictionaryX = %[];
  var timeDictionaryCreate2 = -1;
  if(typeof global.Dictionary.rebuild != "undefined")
  {
    testBegin();
    (global.Dictionary.rebuild incontextof dictionaryX)(CREATE_NUM); // 追加関数
    for(var i = 0; i < CREATE_NUM; i++)
    {
      var name = "hash_number_is_" + "%06d".sprintf(i);
      dictionaryX[name] = i;
    }
    timeDictionaryCreate2 = testEnd();
  }

  // 保存
  testBegin();
  (global.Dictionary.saveStruct incontextof dictionary1)(FILE_NAME);
  var timeDictionarySave = testEnd();

  // 読み込み
  testBegin();
  var dictionary2 = Scripts.evalStorage(FILE_NAME);
  var timeDictionaryLoad = testEnd();

  // アサイン実行(辞書:assign)
  testBegin();
  var dictionary3 = %[];
  (global.Dictionary.assign incontextof dictionary3)(dictionary1);
  var timeDictionaryAssignD = testEnd();

  // アサイン用配列作成
  var dictionary_array = [];
  for(var i = 0; i < CREATE_NUM; i++)
  {
    var name = "hash_number_is_" + "%06d".sprintf(i);
    dictionary_array[i*2] = name;
    dictionary_array[i*2+1] = i;
  }

  // アサイン実行(配列)
  testBegin();
  var dictionary4 = %[];
  (global.Dictionary.assign incontextof dictionary4)(dictionary_array);
  var timeDictionaryAssignA = testEnd();

  // アサイン実行(辞書:assignStruct)
  testBegin();
  var dictionary5 = %[];
  (global.Dictionary.assignStruct incontextof dictionary5)(dictionary1);
  var timeDictionaryAssignS = testEnd();

  // 辞書検索時間測定
  testBegin();
  for (var i = 0; i < FIND_NUM; i++)
    temp = dictionary1["hash_number_is_" + "%06d".sprintf(findTable[i])];
  var timeDictionaryFind = testEnd();

  // 辞書検索時間測定(指定数でRebuildHashしたもの)
  var timeDictionaryFind2 = -1;
  if(timeDictionaryCreate2 != -1)
  {
    testBegin();
    for (var i = 0; i < FIND_NUM; i++)
      temp = dictionaryX["hash_number_is_" + "%06d".sprintf(findTable[i])];
    timeDictionaryFind2 = testEnd();
  }

  //------------------------------------------------------------------
  // 記録
  //------------------------------------------------------------------

  data_timeArrayCreate[testCase][count] = timeArrayCreate;
  data_timeArraySave[testCase][count] = timeArraySave;
  data_timeArrayLoad[testCase][count] = timeArrayLoad;
  data_timeArrayAssignA[testCase][count] = timeArrayAssignA;
  data_timeArrayFind[testCase][count] = timeArrayFind;

  data_timeDictionaryCreate[testCase][count] = timeDictionaryCreate;
  data_timeDictionaryCreate2[testCase][count] = timeDictionaryCreate2;
  data_timeDictionarySave[testCase][count] = timeDictionarySave;
  data_timeDictionaryLoad[testCase][count] = timeDictionaryLoad;
  data_timeDictionaryAssignD[testCase][count] = timeDictionaryAssignD;
  data_timeDictionaryAssignA[testCase][count] = timeDictionaryAssignA;
  data_timeDictionaryAssignS[testCase][count] = timeDictionaryAssignS;;
  data_timeDictionaryFind[testCase][count] = timeDictionaryFind;
  data_timeDictionaryFind2[testCase][count] = timeDictionaryFind2;

  //------------------------------------------------------------------
  // 後始末
  //------------------------------------------------------------------

  invalidate findTable;

  invalidate array1;
  invalidate array2;
  invalidate array3;

  invalidate dictionary1;
  invalidate dictionary2;
  invalidate dictionary3;
  invalidate dictionary4;
  invalidate dictionary5;
  invalidate dictionary_array;
  invalidate dictionaryX;
}

//////////////////////////////////////////////////////////////////////
// テストループ
//////////////////////////////////////////////////////////////////////

var data_createNum = [];

var data_timeArrayCreate = [];
var data_timeArraySave = [];
var data_timeArrayLoad = [];
var data_timeArrayAssignA = [];
var data_timeArrayFind = [];
var data_timeDictionaryCreate = [];
var data_timeDictionaryCreate2 = [];
var data_timeDictionarySave = [];
var data_timeDictionaryLoad = [];
var data_timeDictionaryAssignD = [];
var data_timeDictionaryAssignA = [];
var data_timeDictionaryAssignS = [];
var data_timeDictionaryFind = [];
var data_timeDictionaryFind2 = [];

var testCase = 0;
for(var CreateNum=100000; CreateNum<=100000; CreateNum+=10000)
{
  data_createNum[testCase] = CreateNum;

  // 計測データを格納する配列を作成  
  data_timeArrayCreate[testCase] = new Array();
  data_timeArraySave[testCase] = new Array();
  data_timeArrayLoad[testCase] = new Array();
  data_timeArrayAssignA[testCase] = new Array();
  data_timeArrayFind[testCase] = new Array();
  data_timeDictionaryCreate[testCase] = new Array();
  data_timeDictionaryCreate2[testCase] = new Array();
  data_timeDictionarySave[testCase] = new Array();
  data_timeDictionaryLoad[testCase] = new Array();
  data_timeDictionaryAssignD[testCase] = new Array();
  data_timeDictionaryAssignA[testCase] = new Array();
  data_timeDictionaryAssignS[testCase] = new Array();
  data_timeDictionaryFind[testCase] = new Array();
  data_timeDictionaryFind2[testCase] = new Array();

  // 割り込みによって値にぶれが出る場合があるので
  // 同じケースについて何度か計測する
  // 計測したうちのもっとも小さい値を使う
  for(var count=0; count<5; count++)
  {
    testMain(CreateNum, testCase, count);
  }
  
  // 計測データを昇順にソート  
  data_timeArrayCreate[testCase].sort();
  data_timeArraySave[testCase].sort();
  data_timeArrayLoad[testCase].sort();
  data_timeArrayAssignA[testCase].sort();
  data_timeArrayFind[testCase].sort();
  data_timeDictionaryCreate[testCase].sort();
  data_timeDictionaryCreate2[testCase].sort();
  data_timeDictionarySave[testCase].sort();
  data_timeDictionaryLoad[testCase].sort();
  data_timeDictionaryAssignD[testCase].sort();
  data_timeDictionaryAssignA[testCase].sort();
  data_timeDictionaryAssignS[testCase].sort();
  data_timeDictionaryFind[testCase].sort();
  data_timeDictionaryFind2[testCase].sort();

  // 出力
  Debug.message("===========================================");
  Debug.message("create num           : %8d"
    .sprintf(data_createNum[testCase]));
  Debug.message("find num             : %8d"
    .sprintf(FIND_NUM));
  Debug.message("array      create    : %8d ms"
    .sprintf(data_timeArrayCreate[testCase][0]));
  Debug.message("array      save      : %8d ms"
    .sprintf(data_timeArraySave[testCase][0]));
  Debug.message("array      load      : %8d ms"
    .sprintf(data_timeArrayLoad[testCase][0]));
  Debug.message("array      assignA   : %8d ms ※arrayをassign"
    .sprintf(data_timeArrayAssignA[testCase][0]));
  Debug.message("array      find      : %8d ms"
    .sprintf(data_timeArrayFind[testCase][0]));
  Debug.message("dictionary create    : %8d ms"
    .sprintf(data_timeDictionaryCreate[testCase][0]));
  Debug.message("dictionary create2   : %8d ms ※予めrebuildしたもの"
    .sprintf(data_timeDictionaryCreate2[testCase][0]));
  Debug.message("dictionary save      : %8d ms"
    .sprintf(data_timeDictionarySave[testCase][0]));
  Debug.message("dictionary load      : %8d ms"
    .sprintf(data_timeDictionaryLoad[testCase][0]));
  Debug.message("dictionary assignD   : %8d ms ※dictionaryをassign"
    .sprintf(data_timeDictionaryAssignD[testCase][0]));
  Debug.message("dictionary assignA   : %8d ms ※arrayをassign"
    .sprintf(data_timeDictionaryAssignA[testCase][0]));
  Debug.message("dictionary assignS   : %8d ms ※dictionaryをassignStruct"
    .sprintf(data_timeDictionaryAssignS[testCase][0]));
  Debug.message("dictionary find      : %8d ms"
    .sprintf(data_timeDictionaryFind[testCase][0]));
  Debug.message("dictionary find2     : %8d ms ※予めrebuildしたもの"
    .sprintf(data_timeDictionaryFind2[testCase][0]));
  Debug.message("===========================================");

  testCase++;
}

ループしてないfor文や、いちいちデータを蓄えてる無駄っぽい処理はグラフ作成用である。
まずは改造前のkrkr.exeで速度を計測してみよう。
手持ちの2.32.2.426で試した。


create num           :   100000
find num             :     1000
array      create    :       77 ms
array      save      :      515 ms
array      load      :      127 ms
array      assignA   :        1 ms ※arrayをassign
array      find      :     5311 ms
dictionary create    :     5685 ms
dictionary create2   :       -1 ms ※予めrebuildしたもの
dictionary save      :      976 ms
dictionary load      :     2579 ms
dictionary assignD   :     1355 ms ※dictionaryをassign
dictionary assignA   :     5252 ms ※arrayをassign
dictionary assignS   :     1327 ms ※dictionaryをassignStruct
dictionary find      :       85 ms
dictionary find2     :       -1 ms ※予めrebuildしたもの

そしてこちらが改造版。


create num           :   100000
find num             :     1000
array      create    :       90 ms
array      save      :      549 ms
array      load      :      121 ms
array      assignA   :        2 ms ※arrayをassign
array      find      :     5402 ms
dictionary create    :     5646 ms
dictionary create2   :       88 ms ※予めrebuildしたもの
dictionary save      :     1025 ms
dictionary load      :      216 ms
dictionary assignD   :       32 ms ※dictionaryをassign
dictionary assignA   :       18 ms ※arrayをassign
dictionary assignS   :       32 ms ※dictionaryをassignStruct 
dictionary find      :       86 ms
dictionary find2     :        1 ms ※予めrebuildしたもの

軽く10~100倍速くなってる!

ちょっと驚きの数字だが、もっともこれはあくまで追加する要素数が10万個の場合の数値である。
辞書の作成が重い原因は、リストが伸びれば伸びるほど検索に時間がかかることに起因しているので
要素数に対する処理時間のグラフは2次曲線を描く。
またキーとなっている文字列の長さも大きく影響している。
つまり追加する要素数がめちゃくちゃ多いからこそこれほど劇的な差になっているのであって
これをもってして辞書が超高速になったと思うのは早合点である。

しかしこの高速化策によって要素数に対する処理時間のグラフは一次曲線(直線)になる。
この高速化策は導入コストもリスクも低いので、ほとんどの場合有用だろう。
実際にグラフを作ってみたのでご覧いただきたい。

改造前
DictionarySpeedTest1

改造版
DictionarySpeedTest2

検索のグラフがちょっとがくがくしてるが、検索回数が少ないのと、
検索をランダムにしているためだろう。検索回数は1000回固定である。

まとめると…
Dictionaryは決して遅くはない。むしろ速い。
しかし使い方を誤ると極端に遅くなってしまう場合がある。
けれどもその仕組みを理解し適切に扱うことで問題を回避できる。
ということである。

ご参考までに。