2015年12月27日日曜日

C#での文字列switchとDictionaryとReflectionとで速度比較してみた

やる前から結論は見えているようにも思えますが、MSVS2015の最適化能力とはいかばかりか気になったのであえてやってみました。

今回のケースは次の通りです。
  • クラス1つにつき1回のデータが入っていて、データ(メンバ変数)数は500。
    イメージとしては1クラス/1レコードといった具合です。
    但しそのデータはクラス内クラスによって区分されて保存されている。
    たとえば、
    class a{
      class aa{
       public float val1;
       class aaa{
        public int val2;
       }
       ...
       class az{
        public int val99;
       }

    のように。こういう具合で細分化された項目数の合計が500あります。
  • そのレコードが数万程度リストとして保持されている。
  • そのリストを、メンバ名で(イメージとしてはカラム名で)で横ぐしで集計したい。
という具合です。

クラス設計が悪い?
まったくその通りですが、文句を言っても解決にならんので、とりあえずなんか考えてみたいと思います。

いろいろ手法があると思います。クラスの構造をのべたんに開いて、ってこりゃ方言かな?、構造を展開してしまうとか別にリストを作り直すとか。

ここでは、あえて強引にメンバ名で当該クラスにアクセスしてデータを取得するという手法をとってみることにすることにします。イメージとしてはXPath式でデータにアクセスするような感じで。

当然、この場合、考えつくのはリフレクションを用いることです。
そして、まさしく当然のごとくアクセスできます。便利です。

ですが、リフレクションは遲いというのがもっぱらの評価です。
そこで、今回の比較と相成ります。

Case1. switch( メンバ名 ) の場合。
public float get(string member)
        {
            switch ( member )
            {
                case "aa.val1":
                    return aa.val1;

のような延々と500ものcase文が続く壮絶な関数を作って時間を計測してみました。(時間はあとでまとめて書きます。)

Case2. Dictionaryを用いた場合。
dic = new Dictionary<string, float>() {
                    { "aa.val1", aa.val1 },
  といった具合に、データを保持しているクラスがデータを収集し終えた段階で辞書をこさえておいて、アクセスする場合にはその辞書に対してキーとしてメンバ名を渡して値を得る方法です。

Case3. リフレクションを用いた場合。
単純にInvokeMemberメソッドでGetFieldする方法です。

上記の3例でかかった時間をStopwatchクラスで計測しました。(Core i7/たしか4770)
  1. クラスから1回全要素を取得するループの実行時間
     switchの場合:     50ms
     Dictionaryの場合: 12ms
     Reflectionの場合: 1ms
  2. 10回全要素を取得するループの実行時間
     switchの場合:     83ms
     Dictionaryの場合: 11ms
     Reflectionの場合: 9ms
  3. 100 回全要素を取得するループの実行時間
     switchの場合:     137ms
     Dictionaryの場合: 15ms
     Reflectionの場合: 94ms
  4. 10000回全要素を取得するループの実行時間
     switchの場合: 1072ms
     Dictionaryの場合: 379ms
     Reflectionの場合: 7938ms
面白い結果になりました。
10回程度のループ処理ならInvokeMemberで値を取得したほうが早かったんです。

逆コンパイルしたらswitch文のコードは文字列のハッシュをとったりして涙ぐましい最適化処理をコンパイラがやってくれていました。
しかし、辞書とリフレクションを用いた関数を利用して取得したほうは書いたままのコードでした。

私はGCがあるような言語は業務で使いたくないので詳しくないのですが、今回のような結果が出るとは思いにもよりませんでした。

面白い!実に奥が深い!!

なぜ回数が少ないとInvokeMemberが早いのか。
今回は考察まで踏み込めませんでした。ごめんなさい。
でも、こんなことがあるから後期中齢者になってもプログラミングがやめられません。

2015年12月17日木曜日

UnityではSystem.IO.Compression.GZipStreamが使えない

UnityではSystem.IO.Compression.GZipStreamが使えない!!
シリアル化したセーブデータを圧縮・伸長したいのに!!

と思ったらきちんと代替手段が用意されていたで御座候。
それどころかbzip2まで用意されてござる(遅いから今回は使わないけど)。

しかも、(おそらく)セーブデータなどでの圧縮・伸長用に考慮されているとみえ、streamとして扱える丁寧なつくりです。

私事ながら現在手掛けているC:SL用MODはシリアル化するとかなり大きくなるデータを扱うMODなのでSystem.IO.Compression名前空間のお友達に会えないとわかった時にはどうしようかと思いましたが、このおかげでセーブデータ長制限に引っかかる率も格段に下がってありがたい限りです。

使い方は超簡単。例えばリストア先クラスfooがあったとして、速度重視でgzipで伸長してシリアルデータからオブジェクトに戻す場合は、
BinaryFormatter bf = new BinaryFormatter();
using ( MemoryStream ms = new MemoryStream( savedata ) ){
  using ( GZipInputStream gs = new GZipInputStream( ms ) ){
    foo = bf.Deserialize( gs ) );
  }
}
MemoryStreamをコンストラクタへの引数にするだけ。
間に一枚かぶせるだけで済みます。圧縮(GZipOutputStream)の場合もまた然り。

・・・確かに標準のライブラリより多機能かもしれないけど・・・メンドクセ

2015年12月11日金曜日

Cities: SkylinesのmonoランタイムにはSystem.Type.op_Equalityメソッドがない

ようやくロード・セーブの方式も確立して、多言語化もほぼサポートして、基本的な雛形が完成したので、さあこれからだ!というところでいきなり躓きました。

データ保持用のクラスに項目を追加しただけで急に動かなくなりました。

Cities: Skylinesでのデバッグは仮に例外が発生していても本体側で握りつぶしてしまうのでどこで起きているかわかりません。
そのうえ、Unityの仕様だと思いますが、プロセスにアタッチしてもソースレベルではデバッグが不可能。
デバッグコンソールか自力でエラーログをファイルに吐くしかなくて、さらにスタックトレースを出そうとしても、トレース情報がランタイムによって消去されてしまって表示不可能ときた。

提供されているクラスライブラリのドキュメントがどこにもない(どう探しても見つからない!!)ので試行錯誤や諸先輩方の公開されているソースだけに頼っている上に、いまどきprintfデバッグが必要というとんでもない代物なのでなかなか進みませんが、ただメンバを追加しただけなのに急に動かなるとかいったいどういうことが起きているのかさっぱりわかりませんでした。

とりあえず怪しそうなところをprintfデバッグで絞り込んでcatchしてみると
MissingMethodException: Method not found: 'System.Type.op_Equality'
なる例外が。

このエラーは追加したクラスのメソッドを呼び出す際に発生していることが分かりました。
呼び出されるメソッドはただコンストラクタから呼び出されて変数の初期化をするだけの変哲もないもので、そもそもこんなメソッドを呼んだこともないし見たこともないので面喰いました。

調べてみると、これは.Net Framework v4で追加されたものだそうですが、monoはv4に対応しているものの、Cities: Skylinesの採用するミドルウェア(Unity)が古いmonoを採用しているため、v3.5相当の機能しか使えないことがやっとわかりました。

VisualStudio2015でビルドまでしてしまっていたので、v4.6がターゲットになっていたため、このトラブルに見舞われました。

最初からきちんと調べるか、ビルドはCities: Skylines側が提供するバイナリで行えばこのようなトラブルは起きえないのですが、自分の粗忽さから招いた自業自得でした。

また一つ、恥をかいてしまいました。

2015年12月9日水曜日

Cities: Skylinesのmodを作ってみよう

ということで、昨日から作り始めました。

assetじゃないよ。
modとassetをごっちゃになさっている方はそのままブラウザの戻るボタンを押下してくださいませ。

C:SLのmodはC#で書くんだぜ、ということらしいので久しぶりにC#触ったけど、なんだか以前触った時よりものすごく進化していますねえ。びっくりしました。

まあ、別にC#に精通するつもりはないのであんまり深く追っかけないことにして、とりあえずひな形としてIUserModを継承したクラスを作って、NameとDescriptionを返せばとりあえずは体裁が整います。

でもこれだけじゃ意味がないので、UI画面を作るにはUIPanelから継承したクラスを作って、それをLoadingExtensionBaseを継承したクラスメンバのOnLevelLoaded()で、

UIView.GetAView().AddUIComponent( typeof( "UIPanelから継承したクラス" ) )

とするとUI画面ができました。

画面が出てしまえばもうこっちのものなので、今度はその画面に表示するデータをセーブファイルに紛れ込ませる方法は、ISerializableDataExtensionを継承したクラスを作るだけ。

C#のすごいところは、リストを含んでいようがインナークラスを含んでいようが、いとも簡単にシリアライズできてしまったところ。
これは実に驚きました。

うーん、なんて楽なんだ・・・

ポインタがある言語ではクラスや関数が多種類あるけど呼び出し形式もとれるデータ形式も同じだよ、というような場合の処理は関数やメンバ変数へのポインタが大変便利ですが、メモリ管理が自分でできない言語だと多少厄介です。
その点、javaと同様にC#もリフレクションが充実しているようです。
おかげで、メンバ名で直接メンバの値を操作できるような仕組みが実装できました(でもやっぱりポインタのほうが分かりやすいなぁ・・・)。

たとえば、

public class foo{
  public bar piyo ;
  public foo(){
    piyo = new bar();
  }
  public class bar{
    public int hoge;
  }
}

なクラスに"piyo.hoge"というメンバに"文字列で"アクセスしたいなぁ、といったときに

float getValueFromMemberName( object topobj, string member )
{
string[] arrays = member.Split( '.' );
object obj = topobj;
for ( int i = 0 ; i < arrays.Length ; i++ )
{
Type t = obj.GetType();
obj = t.InvokeMember( arrays[i], System.Reflection.BindingFlags.GetField, null, obj, null );
}
return (float)Convert.ChangeType( obj, typeof( float ) );
}
という関数をでっち上げて、topobjに new foo() したオブジェクトを指定して、memberに文字列で"piyo.hoge"と指定してやると、hogeの値が(intなのに)floatで頂戴できるという寸法。
まあ、ポインタが使えない苦肉の策なんですけども、javaとかC#のような言語ならではですねえ。

いまだにCSLのライブラリ群のドキュメントを見つけられていないんですが、VisualStudioのオブジェクトブラウザでメソッドやクラスを眺めているだけでだいたい見当がついちゃったのはこの手の言語ならではでしょうね。

基本的な表示とセーブデータへの私のmod固有のデータのセーブ・ロード方法が分かったので、あとはきちんとUIを作るだけになりました。
ドキュメントが全然ないフレームワークに乗っかっても1日でここまで来るとは思いにもよりませんでした。

あとは、同じようなmodが先に公開されませんように祈りつつコーディング作業を急がなくては。。。