Archive - C#

ZeroFormatter 1.3 - 機能強化とstructの超高速性能とFAQと。

ほとんど昨日の今日な状態で1.3って、バージョン1.0とは何だったのか、というかそれってベータだったということなのでは?という、あまりにいい加減なバージョン番号付けなのですけれど、そんなわけで1.3です。これが本当の1.0だ……。

基本的な概要は初出での記事 ZeroFormatter - C#の最速かつ無限大高速な .NET, .NET Core, Unity用シリアライザーを読んでいただければと思うのですが、では何が変わったかというと、ReadMeを全部書いた!いや地味に面倒なんですよ、分量あるし。英語だし。

というのもあるんですが、方向性を若干変えました。なんというか、反響が思ったよりも良すぎた。あまりの良さにビビッた(GitHub Starも私的最高伸び速度最大をマークした)、のと、だいぶ気を良くしたので、ユースケースを変えたベンチマークを他にとってみたりして、改めて考えた結果「汎用的に全方位に使える最強シリアライザ」にすることにした。というのが大きな方針転換。

汎用シリアライザとして

ビルトインでサポートしてる型を大幅に増やしました。具体的には

All primitives, All enums, TimeSpan, DateTime, DateTimeOffset,
Tuple<,...>, KeyValuePair<,>, KeyTuple<,...>,
Array, List<>, HashSet<>, Dictionary<,>, ReadOnlyCollection<>, ReadOnlyDictionary<,>,
IEnumerable<>, ICollection<>, IList<>, ISet<,>,
IReadOnlyCollection<>, IReadOnlyList<>, IReadOnlyDictionary<,>, ILookup<,>
and inherited ICollection<> with paramterless constructor

です。まぁようするに、普通に生活してて(?)出てくるほとんど全部の型がそのまま使えます。特にコレクション系を、普通に使ってても一切躓かないようにしました。1.0では実はIList/IDictionaryしかサポートしていなかったのです!もともとの発端がFlatBuffersのような内部にバイト配列を抱えてデシリアライズしないから無限大に速い(ツッコミどころの多いこの表現ですが、これはCap’n Protoから引用してます。Cap’n Protoは日本での知名度はゼロに近いですが、私は最初見た時かなり衝撃を受けました。ちなみに他にもタイムトラベルRPCとか、カッコイイ用語が目白押しなのもCap’n Protoは素敵です)、という点を強く意識していたので、具象型(ListとかArray)だと、それが実現できないんですよね。なので却下にしてたのですけれど、「汎用シリアライザ」として使わせたいんだったらサポートしたほうがいいかな、と。シリアライズ/デシリアライズ速度が他を圧倒して超高速だったというのも決断を後押ししてます。まぁこれだけ速いんだから全然いいだろ、みたいな。

structが超速い

というか、これに関しては他が遅すぎるといったほうが正しいぐらい。

image

intだけとかVector3とかそれの配列とか、HTMLぐらいを想定した大きめ文字列とかの結果です。文字列は結局UTF-8でエンコード/デコードするのはみんな変わらないのでそんなもんかってところですが、他が絶望的に違いすぎる。アホみたいに差が開いてるんですが、これは事実なんだなぁ。

これは、小さいデータに関しての考慮が全然ないから、というのがめっちゃ大きい。int(1)を書くってのは、つまり最速は BitConverter.GetBytes(1) なんですよ、で、もはやそこからどれだけ「遅くするか」の勝負ですらある。他のシリアライザは、やってることがあまりにも多い、だから際限なく、最速から遠くなる。ZeroFormatterは限界まで無駄がない(実際、これ以上縮めようがない)ので、もんのすごく差が開きます。どうせ小さいデータだから一個一個は差がデカいといっても小さいとも言えるんですが、頻度が高いと馬鹿にならない差になります。というかさすがにここまで違うと全然違うでしょう。

小さいデータのやり取りって、ないようで結構あるんですよ。ウェブだったら、例えばMemcachedやRedisなどKVSへのアクセスでintだけ格納したりとかって普通によくある。ゲームだったら座標データ(Vector3)のやり取りとかね。なのでまぁ、ZeroFormatterはかなり価値あるかなー、と。

Union型の追加

なにそれ、というと、一個の型の表明で複数の型を返せるようになります。どちらかというとポリモーフィズムのほうが近いですかねー、実際C#でのデシリアライズ結果はポリモーフィズムとしての表現に落としているので。ド直球に言うとFlatBuffersにあるやつです。

// こんなんで判別したいとして
public enum CharacterType
{
    Human, Monster
}
 
// こんなふーにabstract classとUnionAttributeに子クラスを並べて、UnionKeyで識別するものを指します
[Union(typeof(Human), typeof(Monster))]
public abstract class Character
{
    [UnionKey]
    public abstract CharacterType Type { get; }
}
 
// あとは延々と並べる。
[ZeroFormattable]
public class Human : Character
{
    // UnionKeyはintでもstringでもなんでもいいんですが、かならず同じ値が帰ってくるようにする必要がある
    public override CharacterType Type => CharacterType.Human;
 
    [Index(0)]
    public virtual string Name { get; set; }
 
    [Index(1)]
    public virtual DateTime Birth { get; set; }
 
    [Index(2)]
    public virtual int Age { get; set; }
 
    [Index(3)]
    public virtual int Faith { get; set; }
}
 
[ZeroFormattable]
public class Monster : Character
{
    public override CharacterType Type => CharacterType.Monster;
 
    [Index(0)]
    public virtual string Race { get; set; }
 
    [Index(1)]
    public virtual int Power { get; set; }
 
    [Index(2)]
    public virtual int Magic { get; set; }
}
// で、こう使う。
var demon = new Monster { Race = "Demon", Power = 9999, Magic = 1000 };
 
// Union型を指定してシリアライズする(そうしないと子を直接シリアライズしてしまうので)
var data = ZeroFormatterSerializer.Serialize<Character>(demon);
 
var union = ZeroFormatterSerializer.Deserialize<Character>(data);
 
// 結局みんな大好きswitchですが何か。
switch (union.Type)
{
    case CharacterType.Monster:
        var demon2 = (Monster)union;
        demon2.Race...
        demon2.Power..
        demon2.Magic...
        break;
    case CharacterType.Human:
        var human2 = (Human)union;
        human2.Name...
        human2.Birth...
        human2.Age..
        human2.Faith...
        break;
    default:
        Assert.Fail("invalid");
        break;
}

最終的にswitchなのがダサいといえばダサいんですが(C#でやる表現上の限界かな!)、まぁ悪くない落とし所なのではないかな、と。で、これ、便利ですよ。マジで。うーん、結構あるんですよね、状況に応じて複数データ返したいときって。で、愚直にやるとこうなるわけです。

public class Hoge
{
    public 何か1の時の型 Nanika1 { get; set;}
    public 何か2の時の型 Nanika2 { get; set;}
    public 何か3の時の型 Nanika3 { get; set;}
}

いやー、色々無駄だし型の表現としてもアレだしちょっと、ねー、っていう。

Unionをシリアライザで記述するという点では、ZeroFormatterのやり方はかなり上手い感じで(自分で言う)、書きやすさと安全性(完全ではないけれど、意識しやすさが高いのでそこそこはある)をいい塩梅に両立させれたんじゃないかなー、と。特に書きやすさはかなりあると思います。というかぶっちけ他のシリアライザでこの手のポリモーフィズムやるのは凄まじく大変なので、革命的に便利になったといっても過言ではない。

バイナリ仕様の整理と多言語対応

諸々の追加や事情も踏まえて、バイナリ仕様を整理しました。

まず、言語中立にしました。いやまぁ、もともと、C#依存度の高いものは外して移植しようと思えばできるように、みたいな感じに作ってはいたのですけれど、より明確に中立を意識して整理しました。元々かなり頭悪く単純に作ってあるので(ZeroFormatterの速さは賢くないバイナリ仕様をC#実装力でねじ伏せる、というところがかなりあって、逆に言えば実装Firstで作られているので、言語実装で最速になるように寄り添って仕様が固まったとも言える)

というのと、↑のように遅延実行ではないコレクションのサポートを正式に入れるということで、Sequence Formatというのを正式に用意して遅延ではないDictionaryなどのレイアウトはここに属する、という形にしました。Objectも、ObjectとStruct という分けかたで定義して、KeyTupleはStructに属してますよ、みたいに割とそこそこちゃんと汎用的感な分類になってるんじゃあなかろうか。結構あーでもないこーでもないと弄ってたんですが、うーん、なるほど、こういうのは結果はあっさりしてるけど過程はとても大変……。

と、いうわけで、言語がC#のみってのはさすがに普通に欠点なんですが、整備してみたんで多言語サポートよろしくお願いします、みたいな(?)。やりたい気持ちはあるんですが、如何せんちょっとC#以外は手が回らないのデスデス。社内ではサーバーもC#で完動するようになってるので、あんまり強い外圧が働かなくて。そして実際手が回らないので。仕様作る!実装する!社内のプロジェクトのデータの移植もする!更にこれを使った次の何かも作る!あわあわわわわあわ、本当に手が回ってないヤヴァイ。

スキーマはあるよ

スキーマはあります。見えないだけで。どういうことかというとこういうことです。

namespace /* Namespace */
{
    // Fomrat Schemna
    [ZeroFormattable]
    public class /* FormatName */
    {
        [Index(/* Index Number */)]
        public virtual /* FormatType */ Name { get; set; }
    }
 
    // UnionSchema
    [Union(typeof(/* Union Subtypes */))]
    public abstract class UnionSchema
    {
        [UnionKey]
        public abstract /* UnionKey Type */ Key { get; }
    }
}

C#自体がスキーマなのです。それの利点はかなりあって、「パーサーを作らなくて済む(C#のコンパイラは既にC#で実装されていて、それのパーサーが使える)」「入力補完/コードフォーマット/シンタックスハイライト/アナライザー拡張などIDE(Visual Studio)の恩恵をフルに使える」ってのが、まずは良い。実際、zfc.exe(ZeroFormatterCompiler)という実行ファイルによって、C#というスキーマをもとにコード生成をしています。現在はAOTのためのC#コード生成ですが、別に出力を変えれば、他の言語のコードでも全然吐けます(ランタイムがないから無理だけど!)

デメリットは「機能が制限されてないので容易に制限からはみだせるので言語中立にしづらい」「現行のC#の言語機能に制限される(例えば非nullなStringは定義できない)」ってとこですね。特に前者がビミョーなんですが紳士協定の範囲内(C#としてコンパイル可能でもZeroFormatterとして解析不能だっていうエラーを放り投げちゃえばSyntaxErrorなコードと変わらない)に収めることはなんとか可能なんじゃあないかなあ、とか。ってのは夢見てます。

そして最大の利点がスキーマが生成を介さなくてもシェアできる、ということ。「プロジェクト参照」や「DLL参照」という形で、スキーマと生成コード(実際は実行時動的生成するんですが)をコード生成なしで複数プロジェクト間で共有できます。シームレスに。これは非常に大きくて、まぁ前の記事でも書いたんですがコード生成はやればやるほど複雑化していくんで、ないに越したことはないんですよね。んで、C# as Schemaだと、ゼロにできる。これはワークフローにとってはインパクトが相当大きいことです。

私は、コード生成や自動化って「したくない」ことの筆頭候補に挙げてます。自動化はミクロでは楽になっても、その積み重ねがマクロでは害悪になるケースが往々にして多い。なので、やるべきことは「自動化をしなくてすむ」ようにすることです。そのために脳みそを動かしたい。結果、脳みそが追いついてなくてそこら中が止まることも往々にしてある。shoganai。

まとめ

redddit/r/csharp/ZeroFormatterでAsk Me Anythingやってます(とは)。Fastestとかぶち撒けたせいでシリアライザ戦争が勃発している(恐ろしい)。なるほどWire、シランカッタ。コード的には基本的にZeroFormatterのほうが速そーなので、トータルで色々なケース作れば勝つと思うんだけど、弱点を突くと負けるケースは出てくるのかなぁ。とはいえ普通に私の手元で図ったら圧勝した、ふむ。(最終的に相手のベンチマークにZeroFormatter足して計測→結果 圧倒的な圧勝ということで、まぁしょうがない、相手が悪い。確かにWireは二位なので、惜しかったで賞というところ)

というわけで、真面目に、C#でサッと今使ってるシリアライザをそのまま置き換えられるものにしました。つまり、あらゆるところで使ってください、と言ってます。実際、小さなところから大きなところまで効果あると思います。小さなところは↑でstructを例にしましたが、大きなところでは、例えばバッチ処理の連鎖とかで、延々と巨大なデータを送っているのだけれど、一つ一つはその一部しか使わないんだよねー、みたいな場合。に、ものすごく効くんじゃない?って意見貰いました。その通りで、実際そういうケースでは正しくめっちゃ効きますねー。

とかとかって感なので、是非是非試してみてくださいな。あとクドい告知ですが11/27開催の歌舞伎座.tech#12「メッセージフォーマット/RPC勉強会」でもお話します&クロスプラットフォーム(Unity, Windows, Mac, Linux)で使える通信用のフレームワークをリリースします(!)のもします(ホントに!)

ZeroFormatter - C#の最速かつ無限大高速な .NET, .NET Core, Unity用シリアライザー

(現状は)C#専用の、新しいシリアライズフォーマットを作りました。アセットストアには置いてないんですが、GitHubで公開しています。ReadMeが超書きかけですが明日ぐらいには全部書き終わってるはず……。

特徴はデシリアライズ速度がゼロなので、真の意味で爆速です。そう、無限大高速。

image

嘘くせー、って話なんですが、実のところこれは類似品があって、Googleの出してるFlatBuffersと基本的な考えは同じです(他にCap’n Protoというのもあります、こっちも元Googleの人ですね)。デシリアライズ「しない」から速い。つまるところ必要になるときまでパースを先送りするってことです。これは、アプリケーションの作りにもよりますが非常に効果があって、例えばデカいマスタデータをドバッと取得するなんてときに、その場で必要なデータってその巨大データのごく一部だったりするんですよね。全部パースしてデシリアライズなんかしてると遅くって。そういった問題をFlatBuffersなら一挙に解決できます(多分)。ってことは同種のZeroFormatterでも大解決できます。

なら、じゃあFlatBuffers使えばいいじゃんって話になると思うんですが、なんとFlatBuffersはAPIがエクストリームすぎて実用はマジ不可能。最速という噂のFlatbuffersの速度のヒミツと、導入方法の紹介という記事にもありますが、まぁfbsという専用IDLでスキーマ書いてジェネレートまでは許せても、その後のオブジェクトの生成をバイナリ弄っているかのように生でビルドさせるのは正気の沙汰ではない……。さすがにこれをふっつーに使うのは無理でしょ、無理。それでもピンとこない?このFlatBuffersの公式サンプルはどうでしょう?new Monster { Hp = … Name =… WEapon… }ってだけのはずのコードが凄いことになってますけれど、まぁ、つまるところそういうことです。厳しい。絶対厳しい。(しかもそんだけやってもそこまで速くないという)。

というわけで、C#で「ちゃんと使える」というのを念頭において、シンプルなAPI(Serialize<T>とDeserialize<T>だけ!)で使えるようにデザインしました。

また、社内的事情で、IDictionaryやILookup(MultiDictionary)へのゼロ速度デシリアライズが欲しかったので(Dictionaryを作るのに配列を全部パースしてC#コードで構築、なんてやってると結局全部パースしててパースの先送りができないわ、Dictionary構築にかなり時間喰っちゃうわで全然ダメ)、ネイティブフォーマットの中にDictionaryやILookupを加えています。これにより爆速でDictionaryのデシリアライズが終わります。Dictionaryをまんま保存できるので、簡易データベース、インメモリKVSとなります。ただのシリアライズフォーマットより少し賢くて、SQLiteのようなデータベースほどは賢くない、けれど、Dictionaryそのものなので絶妙な使いやすさがある。結構、そういうのがマッチするシチュエーションって多いんじゃないかと思います(MySQLをメインに使っててもRedisも最高だよね、みたいな)

シリアライズ速度もまた、並み居る強豪を抑え(protobuf-net, MsgPack-Cli, UnityだとネイティブJsonUtilityなど)最速をマークしています。パフォーマンス系は痛い思い出があるので(性能ガン無視したゴテゴテした何かで構築すると、困ったときに性能を取り戻すのは非常に難しく、始まる前から技術的負債となる……)、とにかくパフォーマンス超優先、絶対落とさん、というぐらいにギチギチに突き詰めました。実際、今後C#でZeroFormatterを超える速度を叩き出すのは不可能でしょう。いやマジで。というぐらいにC#の最適化技法が詰め込んであります。

Unityサポートを最初から組んでいるシリアライザも珍しくて(まぁふつーは.NETでシリアライザ書くとふつーの.NETが対象になって対応が後手に回るので)、使えるっていうだけではなくて、ちゃんとiOS/IL2CPPでも最速が維持できるように組んであります。結果実際、ネイティブ実装なはずのJsonUtilityよりも速い。C#が遅いなんて誰が言ったヲイ。ちゃんと書けば速いのだ(まぁJSONじゃないからってアドバンテージはあるんだけど)。この辺はUniRxの実装で散々IL2CPPと格闘した経験がちゃんと活きてます。

メインターゲットは Server - Unity 間での通信のためですが、Server - ServerのRPC/Microservices的シナリオや、Unityでのファイルセーブなどのシナリオでも有意義に使うことは可能でしょう。難点はネットワーク通信に使うとサーバーもC#で実装しなきゃいけないってことですね!それはいいことですね!この際なのでC#で実装しましょう!そのために .NET Coreにも対応させたのでLinuxでも動かせますよ!

まぁ、この辺は来月ぐらいというか、今月末ぐらいには、更にクロスプラットフォーム(Unity, Windows, Mac, Linux)で使える通信用のフレームワークをリリースします(!)ので、そこはそれを待っていただければきっと活用の幅が広がるはずです……。詳しくは11/27開催の歌舞伎座.tech#12「メッセージフォーマット/RPC勉強会」でお話するつもりなので、是非来てくださいな。

使い方

DLLはNuGetに転がってます。

.NET用。

Unity用。Interfacesは.NET 3.5プロジェクトとUnityで共用できるのでクラスの共通化に使えます。Unityの場合はreleasesからバイナリをダウンロードしてもらったほうがいいかもしれません。

Visual Studio 2015用のAnalyzer。

クラスを定義して、ZeroFormatterSerializer.Serializeでbyte[], DeserializeでTが取れるというのが基本APIになります。

[ZeroFormattable]
public class MyClass
{
    [Index(0)]
    public virtual int Age { get; set; }
 
    [Index(1)]
    public virtual string FirstName { get; set; }
 
    [Index(2)]
    public virtual string LastName { get; set; }
 
    [IgnoreFormat]
    public string FullName { get { return FirstName + LastName; } }
 
    [Index(3)]
    public virtual IList<int> List { get; set; }
}
 
class Program
{
    static void Main(string[] args)
    {
        var mc = new MyClass
        {
            Age = 99,
            FirstName = "hoge",
            LastName = "huga",
            List = new List<int> { 1, 10, 100 }
        };
 
        var bytes = ZeroFormatterSerializer.Serialize(mc);
        var mc2 = ZeroFormatterSerializer.Deserialize<MyClass>(bytes);
 
        // ZeroFormatter.DynamicObjectSegments.MyClass
        Console.WriteLine(mc2.GetType().FullName);
    }
}

ZeroFormatterSerializerの使い方自体は超単純なんですが、対象となるクラスには幾つか制限があります。「ZeroFormattable」で「Indexで番号のついた」「virtualな」プロパティが必要です。更にコレクションはIList<T>で、ディクショナリはIDicitionary<TKey,TValue>で宣言しておく必要があります。おー、なんか面倒くさそうですね!そこでVisual Studioの環境ならAnalyzerが用意されていて、エディット時にリアルタイムで警告/修正してもらえます。

zeroformatteranalyzer

structにも対応していて、その場合は「Indexで0から欠番なしの連番がついたpublicなフィールドかプロパティ」と「その順番どおりの引数を持つコンストラクタ」が要求されます。これもAnalyzerが警告します。詳しいルールはReadMeで!

IDL経由で書くよりマシだし(普通のC#ですからね)、そこまで面倒くさくはないかなあ、というギリギリラインにしているつもりです。virtual強要のダルさとかルールの多さはVisual Studio Analyzerでカバーするという、今風の作りになってます。今風といっても、一つのライブラリにAnalyzerをセットでがっつし組み込むような作りしてる人は私以外見た覚えないけれど……。一昔前だとvirtual強要とか無理ゲーと思ってましたが、Analyzer以降の世代のC#ならこういう作りをしてもアリだなって思えているので、APIの見せ方の幅が広がると思うんで、もう少し増えてもいいんじゃないかなーとは思いますね。

デシリアライズと再シリアライズ

グラフ意味ないレベルなんですが、デシリアライズ速度。特に大きい配列とか、サイズがデカければデカいほど無限大に差は開きます。

image

なんでかといえば、裏にbyte[]を持って、ラップするクラスに包んでいるだけだからです。クラス定義で virtual を強要しているのは、デシリアライズ後のオブジェクトの実態は、裏で動的に作り変えてあり、それを継承したクラスにするためです。FlatBuffersと同等のパフォーマンスでありながら、極力自然なC#のシリアライズ/デシリアライズのAPIに載せるための手段です。

所詮はパースの先送りなので、全部の要素を使う場合はそこまで差は開きません(但し、ふつーのbyte[]から実体化するという点でのデシリアライズもかなり高速なので、仮に全プロパティを舐めても他のシリアライザよりも速度的には高速になってます、現状の実装だと)。まぁ、モノによってはすんごく効果的というのは分かってもらえるかと。実際うちの(開発中の)ゲームでは効果大(になる見込み)です。

そうして作り込んだオブジェクトの再シリアライズも強烈な速度です、というか、こちらも再シリアライズも無限大高速です。というのも裏で持ってるbyte[]をBuffer.BlockCopyでコピーするだけだから。シリアライズしないから無限大速い。これはひどぅぃ。

再シリアライズするシナリオっていうのは、サーバー側だとMicroservices的な分散環境でかなり効果あると思ってます。オブジェクトを左から右に流すだけって、それなりにあるんですよね。そういう時に生のbyte[]でやりくりするとかじゃなくて、ちゃんとオブジェクトとしての実体を持ちつつ(API的に嬉しい)、パフォーマンスも両立(左から右に流すだけなのでデシリアライズもしなければシリアライズもしない!)することが達成できます。

また、触らないというだけじゃなくて、触ることもできます。オブジェクトはミュータブルで、ちゃんとふつーのクラスのように扱って値も変えられます。FlatBuffersは制限付きで一部だけ可能なんですが、ZeroFormatterは全てを変更可能にしてます(イミュータブルにしたい場合はセッターをprotectedにしたりIListのかわりにIReadOnlyListで宣言したりすることでイミュータブルにできるので安心してくだしあ)。この場合、もし固定長の値(intとかfloatとか)を変更した場合は、裏のbyte[]に直接書き込むので、再シリアライズの高速性は維持されます。可変長の値(stringとかオブジェクトとか)を変更した場合は、そこの部分だけシリアライズが必要な差分としてマークされます。それ以外の箇所はbyte[]をBlockCopyするので、可能な限りの高速性を維持しつつも自由な編集を可能にしています。

これはFlatBuffersでは出来ないし、当然他のフォーマットでもできません。

シリアライズパフォーマンス

シリアライズはデシリアライズの時のようなチートは出来ないんで、ZeroFormatterの実装も正攻法で真正面から競ってます。で、ちゃんと速いというか十分以上に速いですというか.NET最速です、いやほんと。

image

image

せっかくなので比較対象は沢山用意していて、まぁprotobuf-netを基準として見るといいと思います。protobuf-netは実際、.NET最速シリアライザで、良いパフォーマンス出してます。でもZeroFormatterはその2倍以上速いんだなぁ。FsPicklerは個性的で面白いし、機能の豊富さを考えると十分よくやってる感じでいいですね。FlatBuffersは気合が足りないですね、あんだけ奇怪なAPIでこれかよ、みたいな。

Google.Protobufが凄く良好でビックリした。これはGoogle公式のProto3実装で、gRPCとか最近のGoogle公式でProtocol Buffersを多用するものはこれを使っていますね。ただ、汎用シリアライザじゃなくて、protoから生成してやらないと一歩も動けないタイプなので使いづらいとは思います。gRPCとかで完全にIDLのシステムが固まっている場合でなら、速度的に不安にならなくて良いという点で良いですねぇ。というかなんでこんな速いんだろ、いや、速いのはいいんですけど、コード的に、だったらZeroFormatterはもう少しいけるはずなはず、うーん、事前csコード生成で普通にコンパイルかけたほうが動的生成よりイケてるってのはあるにはあるんですが、とはいえとはいえ。むー。

というわけでZeroFormatterは実際超速い、んですが速度の秘訣は、沢山あります!幾つか紹介すると、一つは自分でコントロールできない実装を一切通してないから。ひたすらrefでbyte[]を回して、それに対して書き込むだけって感じになっていて、Streamすら使っていません。(Memory)Streamはあんまり通さないほうがいいですね、基本的にはパフォーマンスのネックになります。実際Google.Protobufやprotobuf-netは内部で書き込み用のbyte[]バッファを持ってて、溢れた時のFlushのタイミングでだけ嫌々(?)MemoryStreamに書きに行ってます。ZeroFormatterは更に徹底して、byte[]だけをひたすら引き回す。

次に、整数(など)が可変長じゃないから。Protocol BuffersやMsgPackはint(4バイト)をシリアライズするにあたって、4バイト使いません。というか使わない場合があります。それぞれのエンコード方式を使って、例えばよく使われる数字なんかは1バイトとか2バイトでシリアライズできたりします。これによってバイナリサイズが縮みます。素晴らしい。が、これはエンコードの一種と考えられるので、そのまんまintの4バイトを突っ込むのに比べてエンコードのコストがかかってます。ZeroFormatterは固定長です(これは別にパフォーマンス稼ぎたいわけじゃなくて、ランダムアクセス・ミュータブルなデシリアライズのために必要だからそうなってるだけなのですけれど)

文字列の取扱いもそこそこ工夫があります。まず、 Encoding.GetBytes(string) でbyte[]取ってストリームにWrite、なんてのはビミョー。そのbyte[]無駄じゃんって話で。GetBytesにはbyte[]を受け取ってそいつに書き込んでくれるオーバーロードがあるので、それを使います。じゃあ単純にbyte[]投げればいいのかっていうとそうでもなくて、byte[]の長さが足りない時に伸ばしてくれたりしないので、事前にちゃんと余裕もった長さにしてあげる必要があります。つまりエンコード後のサイズを知っておく必要がある。ここで Encoding.GetByteCount を大抵の実装は使うんですが、長さ分かるってことは実質エンコードしたようなものじゃん、と。というわけで、ここは Encoding.GetMaxByteCount で確保します。こっちのほうがずっと軽い。そして、別にちょっと大きめに取るのはそんな問題ないんですよ、後続がシリアライズするのに使うかもしれないし、そもそも既に大きめに確保されているかもしれない。

長さが分かっている場合(intしか返さない場合とかVector2しか返さないとか、何気にあるはず)は、返すbyte[]をきっちりそのサイズでしか確保しないという最適化が入っています。余計なバッファなし。これは↑のstringも同様で(stringだけ返すというのは非常によくある!)、その場合だけ大きめに確保はせず、ジャストサイズで返します。この辺をきっちりやってる実装は、ないですね(Streamが根底に入ってるとそもそも出来ないので、ZeroFormatterがbyte[]しか引き回さない戦略取ってるからこそ出来る芸当とも言える)

オブジェクトへのシリアライザは初回に一度だけDynamicAssemblyで型を動的に生成するわけですが、コード生成の外からループのヘルパーを通したりせずに、全てのコードを埋め込んでます。というわけで長めのil.Emitが延々と続いてるんですが、これは手間かけるだけの効果ありますね、最初はExpressionTreeでプロパティ単位でのシリアライザを用意して回してたりしたんですが、全部埋め込みにしたら劇的に良くなりました。こう差が出ると、あんまExpressionTreeで書いたほうがいいよねー、なんて気はなくなりました。

そうして生成したシリアライザのキャッシュにDictionaryは使いません。辞書のルックアップはオーバーヘッドです。.NETで最速の型をキーにした取り出しは、適当な<T>のクラスのスタティック変数から取り出すことです。特に静的コンストラクタはスレッドセーフが保証されているので、lockもいりません。つまりどうすればいいかというと、静的コンストラクタの中でifを書きまくることが絶対の正解です。if連打とか気持ち悪い?いやいや、いいんですよ、こんなんで、むしろこういうのがいいんですよ。

それやると一つの型につき一つのシリアライザしか登録できないのでコンフィグが出来ない!って話になってしまうんですが、今のとこZeroFormatterはそもそもノー・コンフィグなので問題ない(酷い)。というのはともかく、オプション毎に型を作って<TOption, T>という形で登録するっていう手法があります。その場合はオプションの全組み合わせを一つ一つの型として用意するということになります。んなのアホかって思うかもですが、実際にJilはそういう実装になっていて、真面目に現実的な手法です。

Enumの取扱いはかなり厄介で、そもそもToStringは遅くてヤバい。ので、ZeroFormatterは値でしかシリアライズしません。ToStringのキャッシュってのもありますが、じゃあそのキャッシュはどこに置くのって話になってきて(Dictionaryに突っ込むと取ってくるコストかかるので、やるなら専用シリアライザを動的に作ってIL内に文字列埋め込みが最速でしょうね)、やらなくていいならやらないにこしたことはない!

さて、というだけじゃなくて、そもそも実はEnumのUnderlyingTypeへのキャストも汎用的にやろうとするとかなり大変だったり。つまりTEnumをInt32に変換するって奴で、これ、正攻法でうまく(速く)やる手段はないです。そうなると結局動的コード生成するしかないってことになりそうで、その場合ExpressionTreeでサクッと作るのが正攻法なんですが、今回私はCreateDelegateのハックでやりました。例えば、通常は変換できないFunc<int,int>はFunc<T,int>に変換できます。TがEnumの場合、かつCreateDelegate経由の場合のみ。実装バグが、まぁベンリだしいいんじゃね?って感じで仕様として(?)残ったって感じなんですが、まぁ実際ベンリなので良きかな良きかな。ちなみに、これでExpressionTreeとか動的生成が効かないUnityでも行けるぜ!とか思ったら、そもそもUnityだと(古いmonoのコンパイラだと?)エディター上ですら動かなかった……。のでUnityではこのテクニックは使ってなくて、普通にEnumはクラスと同じように事前コードジェネレートの対象に含めてます。

あとは本当にボクシングが絶対に発生しないように書いてあります。アタリマエと思いきや意外と普通にこの辺が甘いコードは少なくなくて、protobuf-netですら秘孔を突くってほどじゃなく普通にボクシング行きのコードパス通せたりします。このボクシング殺すべしはUnityでも徹底していて、一切ボクシングなコードは通りません。どうしても必要そうな場合でもコードジェネレートでシリアライザを徹底的に事前生成させることで完全に回避してます。MsgPack-CliのUnity用コードが、コレクションをobjectで取り出すようにしてたり(汎用的なAOT対策としては、正解なのですが……)なので、Unityで徹頭徹尾やってるものも珍しい部類に入るんじゃないかと思います。

また、そもそもbyte[]を確保しない(外から渡せて縮小もしない)NoAlloc系のAPIも用意してます。外側でBufferPoolとか用意しといてもらえれば、ゴミを全く発生させないシリアライザになります。内部ではヘルパーオブジェクトの生成も全くしてない(最初から最後までbyte[]を引き回すだけでなんとかしてる)ですしね。これはリアルタイム通信書いてる時に、こんなにバンバン通信してる = シリアライザが動きまくってるのに byte[] を使い捨てまくり嫌すぎる、と思ってどうしても用意したかったのでした。まぁさすがにバンバン通信といったってUpdateループのような毎フレとかじゃあないんで、神経質になりすぎっちゃあなりすぎかもですが。

Unityでのパフォーマンス

ZeroFormatter, MsgPack-Cli, JsonUtilityでの計測です。ループ回数は500回でiPhone 6s Plus/IL2CPPで動かした結果です。ZeroFormatter, MsgPack-Cliはコードジェネレート済み、JsonUtilityはstringの後にEncoding.GetBytesでbyte[]を取る/byte[]からの復元を時間に含めてます(この手の使い方だと通常最終的にbyte[]に落とすはずなので)

image

デシリアライズは例によってチートなので見なくていいんですが、シリアライズもきっちり爆速です。というかJsonUtilityよりも速い。配列がMsgPack-Cliの10倍速い……(MsgPack-Cliの配列のデシリアライズ速度は正直結構厳しい結果ですね、うーん、なんでそうなのかは分からなくもなくはないんですが……)。

ZeroFormatterをUnityで使うには zfc.exe というコンソールアプリケーションを使ってシリアライザを事前生成します。今のところzfcはWindowsでしか動きません(本当は.NET Coreで実装してLinuxやMacでも動かせるようにしたかったんですが、コード解析に使っているRoslynのプロジェクト解析部分がWindows用しかまともに動かせないという鬼門があり、解決策は今のところない。もう少し.NET Coreが成熟すればいい感じになれるはず、まだ実は細かいところがイケてないのだ……)。生成物自体はどのプラットフォームでもいけます。

Unityでパフォーマンスが有利になる点といえば、ZeroFormatterでのデシリアライズ後のDictionaryは普通に書いたDictionaryよりも良い場合があります。何かというと、Unityの場合、Enumがキーの場合のDictionaryはパフォーマンスに不利です。というのも、参照の度に裏でボクシングが発生しているから(これはmonoの古いバージョンのEqualityComparer.Defaultの実装に問題があって、というかふつーの.NETのほうも4.5辺りまで微妙な実装でした)。で、解決策は専用のEqualityComparerを作ってセットしてあげること、です。面倒くさくてやってられないし実際それ分かっててもやれ(て)ないんですが、zfcで生成したDictionaryには専用のEqualityComparerが最初から自動でセットされてます。ので、その問題は起こりません。

ZeroFormatterはUnityのVector3とかはそのまんまだとシリアライズできないんですが、ZeroFormattableなstructに見せかけてzfcを通すと、Vector3用のシリアライザとかを作ってくれてベンリです。例えば

#if INCLUDE_ONLY_CODE_GENERATION
 
using ZeroFormatter;
 
namespace UnityEngine
{
    [ZeroFormattable]
    public struct Vector2
    {
        [Index(0)]
        public float x;
        [Index(1)]
        public float y;
 
        public Vector2(float x, float y)
        {
            this.x = x;
            this.y = y;
        }
    }
}
 
#endif

のようなコードを用意しておくと、「INCLUDE_ONLY_CODE_GENERATION」が特別なシンボルになっていて、zfcのみで解析対象になってVector2用のシリアライザが生成されます。サーバー側でも受けたいとかって場合は、普通に↑のものをそのまま使えばそれはそれでOKです。ラップした何かに置き換える、なんてのは当然オーバーヘッドなわけなので、structがそのまま使えるんならそれにこしたことはないですからねえ。

zfcの解析対象はソースコードです。昔はコンパイル済みのDLLバイナリを解析するコードをよく書いていたのですが、CIでのビルドと相性が悪すぎて(ビルド順序の依存がある・互いが生成しあって、その生成物を参照しているような場合だとCI上でビルド不能になったりする)イマイチでした。というわけで、今後はコードジェネレートはソースコード解析によるものを主軸にしていこうと思っています。まぁ、そもそもコード生成なんてしないにこしたことはないんですけどね、ILでもなんでもいいから極力実行時動的生成にして、UnityのIL2CPP用だとか、特別な理由がある時だけ「しょうがないから」ソースコード生成する、ぐらいがいいと思ってます。全然、コード生成なんてほんといいもんでもなんでもないし、少なくするにこしたことはない。だから私はIDLを定義して生成するってのは嫌いで、C#そのものがIDLにならなきゃならないと思ってます。言語中立にしたいなら、その場合だけ「しょうがないから」IDLをジェネレートすればいい。そうですねぇ、仮にZeroFormatterを言語中立に拡大していくのだとしたら、C#をIDLの代わりにします。csx(C# Script)で直接コンパイルできるような。結構面白いと思うんだよね。

バイナリサイズ

バイナリサイズはMsgPackやProtocol Buffersに比べて「大きい」です。さすがにJSONよりは小さくなるんですが、まぁFlatBuffersとは同じぐらいですね。別にバイナリだから小さいなんてことはなくて、そう、FlatBuffersも結構大きいですよ。なんで大きいのかっていうと、ランダムアクセスするためのヘッダ領域が必要なので、その分が純粋にオーバーヘッドになってます。これはねえ、しゃーない。デシリアライズ先送りのための必要経費です。銀の弾丸なんてこの世にはなくて、トレードオフなんです、トレードオフ。gzipとかLZ4とかで圧縮しちゃうんなら結構縮められるので、もとよりMsgPack+gzipとかってやってるんなら、そんなにサイズに差は出てこないでしょう。せっかくの速度がウリのフォーマットなので、圧縮する場合はLZ4がお薦めです。結局、デシリアライズが速いといってもネットワーク転送量が多くなってしまえば、ネットワーク通信がボトルネックになってトータル処理時間では負けた!みたいなことだって普通に起こるわけなんで、全然、LZ4で圧縮ってのは良い選択です。ていうか私も(モノによってやるやらないの判断は入れますけれど)やります。

また、パフォーマンスのところで有利になると書いた可変長整数「ではない」ことは、バイナリサイズには当然響いてきます。固定長なのはパフォーマンスのためじゃなくてミュータブルにするためだったり、固定長配列の長さを真に固定するために必要だったりするのでしょうがないんですけれどね(FlatBuffersも勿論同様の話で、固定長で整数のサイズが大きくなってしまうのも必要経費でバイナリ仕様的にしょーがない)、どちらかというとパフォーマンスのほうが副産物で。

ところで突然ちなみにBinaryFormatterは更にもっとサイズでかいです、なんでかっていうとかなりリッチめに型情報が入ってるからなんですねえ。シリアライズ/デシリアライズも遅いんで、アレは使わないほうがいいですよ。

他のシリアライザにはないZeroFormatterだけのお薦め機能として、IDictionaryやILookup(MultiDictionary)へのゼロ速度デシリアライズというのを持っているんですが、なんと、それを使うとバイナリサイズが飛躍的に増大します!(ついでにシリアライズ速度も大きく低下する)。なんでかっていうと、中のハッシュテーブルを丸ごとシリアライズしてるので純粋にKey, Valueだけのシリアライズに比べて、結構に大きくなっちゃいます。なのでデフォルトでは有効になってなくて(?)、IDictionaryのかわりにILazyDictionary, ILookupのかわりにILazyLookupという形で型を宣言すると、そっちのモードでシリアライズします。これはトレードオフはトレードオフでも、ちゃんと理解した上で選択しないと危なっかしいので、デフォのIDictionaryは初回アクセス時に丸ごと構築するという、全然遅延してないじゃんモードになってます。

拡張性

ZeroFormatterはバイナリ生成のためのフレームワーク、ぐらいの気持ちで設計してあって、割とサクッと拡張して俺々バイナリを統合して流し込めるようになってます。というのも、ゲーム用に使うというのも主眼に入れてるので、一部の型は汎用ではなくて、特化したバイナリを流したいって局面は全然あるでしょう。拡張のコードの例として、Guidはデフォでサポートしてないんでプロパティの型に使うと怒られるんですが、

// こんな風にFormatter<T>を継承したクラスを作って
public class GuidFormatter : Formatter<Guid>
{
    // もしバイナリが固定サイズなら数字を、そうじゃないならnullを返す
    public override int? GetLength()
    {
        return 16;
    }
 
    // あとはbyte[]に対して書き込む/読み込む
    // BinaryUtilが汎用的に使えるヘルパーになっている他、Formatter<T>.Defaultを呼べば子シリアライザを使える
    public override int Serialize(ref byte[] bytes, int offset, Guid value)
    { 
        return BinaryUtil.WriteBytes(ref bytes, offset, value.ToByteArray());
    }
 
    public override Guid Deserialize(ref byte[] bytes, int offset, DirtyTracker tracker, out int byteSize)
    {
        byteSize = 16;
        var guidBytes = BinaryUtil.ReadBytes(ref bytes, offset, 16);
        return new Guid(guidBytes);
    }
}
 
// どっか起動時に↓のコードを呼んでおけば、Guidに対するデシリアライズが必要な時には↑のコードが呼ばれるようになる
ZeroFormatter.Formatters.Formatter<Guid>.Register(new GuidFormatter());

という風にすれば、どんな型でも対応させられます。ジェネリクス対応や動的に変動させたい、とかって場合のための登録の口も用意されているので(詳しくはReadMeを読んでね!)基本的にはどんな状況でもいけます。社内からはF#の判別共用体へのシリアライズを対応させるって話もありましたが果たして実装してもらえるのであろうか……。

この辺、protobufとかだとバイナリ仕様決まってるので、あんまり手をいれるのは気が引ける、って感じになりますが、新興フォーマットなだけに、別に自由にやっていいんじゃよ、って気になれます。MsgPackにも仕様の中にExtension typeありますけれど、如何せんZeroFormatterはオプションがない直線番長なので、考えることもまったくなく、とにかくbyte[]に書きたいように書けばそれでOK、問題なくちゃんと動きますよ、っていうのが嬉しい話です。

他言語サポート

ないです!私自身はちょっと出来ないので、気になる人がいれば、やっていただける人をゆるぼです。基本的なのは実のところかなり単純で、そこまでC#特化の何かを入れているわけでもなかったりします(というか、一応は汎用的なものを意識しているのでC#特化のものは極力入れてません)。独自のデータ構造が必要になる遅延Dictionaryとかが厳しいんですが(あと、あれはフォーマット的にも内部構造をベタシリアライズしているので、実装しづらさがかなりある)。一応、仕様サポートのステージは考えていて

  • Stage1: 全てが先行評価される(無限大高速なほうの仕様は満たさない)、Decimal, LazyDictionary/LazyMultiDictionaryは非サポート
  • Stage2: リスト、クラスが遅延評価される(無限大高速なデシリアライズ)、Decimal, LazyDictionary/LazyMultiDictionaryは非サポート
  • Stage3: Decimalをサポートする、LazyDictionary/LazyMultiDictionaryは非サポート
  • Stage4: 全フォーマットをサポートする

みたいな感じです。もし、やろう!という方がいれば、まずはStage1から試みてもらえるとどうでしょうかー。バイナリ仕様はGitHubのReadMeにあります。

まとめ

デシリアライズ先送りが魅力なのは勿論なのですが、先送りしないようなものであっても、他より高いパフォーマンスが出るので、ほぼ全方位に有効なものになってるんじゃないかと思います。比較対象としてやたらFlatBuffersに関して言及しましたが、実際のところ本当にあれ実用で使うのは無理なので(あんなんで普通に使えてる人いるのかな……)、まともに使える代物としては唯一無二な価値はあるんじゃないかな、と。

なんで作ろうかって思ったというと、絶賛開発中のゲームで手詰まったからなんですね、とほほ。巨大なDictionary/MultiDictionaryをデータベース代わりに起動時に構築する、というアプローチだったんですが、かなり破綻してて(起動時間は遅いし、そのための対応のせいでただでさえ未熟なワークフローが更にグチャグチャに)、gdgdループの根底にいたのが其奴なのであった。といっても、今更もう作りは変えられないんで、なんというか、なんとかするしかないわけで、ウルトラC的なアプローチに走ったのであった。そりゃ私だって別にこのレイヤーで俺々フォーマット作りたいなんて思わないですよ、んなもん常識的に考えて悪手に決まってるじゃん。他人がやるって言ったら全力で止めるわ。まぁ結果オーライで最終的にはZeroFormatterを活かした爆速仕様になる(予定)んで、いいってことよってことですかね。

元々はそうした無限大高速なデシリアライズと、ボトルネックにならない程度に普通に高速なシリアライズ、ぐらいに思っていたんですが、シリアライズの計測結果がかなり良かったので欲張って、いっそもうやるなら世界最速だろうとガッチガッチに実装し始めると性能は確かに伸びる。やればやるほど伸びる。が、実装時間も伸びる。やればやるほど。なるほど。とはいえ、実は告知してないだけでGitHub上ではpublicにしていたので、社外でも何人かの方には公開を伝えていて、ベータテスターじゃないけれど、様々なフィードバックなどなどを頂きました。それがなければ、全然もっと出来は悪かったと思うので、非常に感謝です。過去に制作した30のライブラリから見るC#コーディングテクニックと個人OSSの原理原則で偉そうに言いましたけれど、自分一人の限界を超えていけるのもいいことですね。外に出すってことで外圧も感じられるし:)MsgPack-Cliの藤原さんがセッションで言ってました気がしましたが、シリアライザーなんて作るのは奇特で、確かにちょっともう次はやりたくない、しんどいー。ILも一生分書いた気がする。しかし、例によって様々な既存シリアライザーの仕様から、それぞれのC#版の実装のコードを大量に読んだので、シリアライザーに関しては更に相当詳しくなりました、ううむ。シリアライザーとは結構ブログでことあるごとに記事書いてたりと、なんか長い付き合いなんですよねえ、最終的にまさか自分で作ることになるとは……。

と、そんなわけなので、是非是非使ってみてください。実用品なのかどうかで言ったら、会社で使う気満々というかそのための代物なので、その辺の耐久性はあります、まぁまだリリースされてないので(!)、耐久性はどんどん上がっていきます、ぐらいで。バグあればどんどん直すというのと、(UniRxで聞かれたことがあるのですが)社内用と社外用に分けてたりもないので、ちゃんとpublicなところでメンテナンスは続いていきます。

現状どうしても通信用フォーマットとしてはC#オンリーなので、サーバー側で送り出せなくて使いにくい、ということも絶対あるとは思うんですが、そのための解決策としてクロスプラットフォーム(Unity, Windows, Mac, Linux)で使えるC#製の通信用フレームワークをリリースする、という計画も控えているので、その辺も含めて注視していただければですね。繰り返しますが、その辺のところは11/27開催の歌舞伎座.tech#12「メッセージフォーマット/RPC勉強会」でお話するつもりなので、是非来てくださいな。

UnityのMonoアップグレードによるasync/awaitを更にUniRxで対応させる

ついに!.NET 4.6アップグレードが始まりました。Unityの。Unity 5.5でC#コンパイラをアップグレードしていましたが、今回はついにフレームワークも、です。また、Unity 5.5のものはC#のバージョンは4に制限されていましたが、今回はC# 6が使えます。現在はForumでアーリアクセスバージョンが公開されていて、ついでにそこでリンクされているVisual Studio Tools for Unityも入れると、かなりふつーに.NET 4.6, C# 6対応で書ける感じです。

さて、.NET 4.6, C# 6といったら非同期。async/await。もちろん、書けました。が、しかし。

async Task ThraedingError()
{
    Debug.Log($"Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
 
    await Task.Delay(TimeSpan.FromMilliseconds(300));
 
    Debug.Log($"From another thread, can't touch transform position. ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    Debug.Log(this.transform.position); // exception
}

これはtransformのとこで例外でます。なんでかっていうと、awaitによって別スレッドに行ってしまっているから。へー。この辺、async/awaitではSynchronizationContextという仕組みで制御するのですが、現在のUnity標準では特に何もされてないようです。

それだけだとアレなので、そこで出てくるのがUniRx。今日、アセットストアで最新バージョンのVer 5.5.0を公開したのですが、この5.5.0ではasync/await対応を試験的に入れています。それによって自動的にSynchronizationContextも生成/登録してくれます。

async Task UseUniRxInBackground()
{
    Debug.Log($"Start ThreadId:{ Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(TimeSpan.FromMilliseconds(300));
    Debug.Log($"From same thread, because UniRx installs UniRxSynchronizationContext.ThreadId:{ Thread.CurrentThread.ManagedThreadId}");
    Debug.Log(this.transform.position); // show transform
}

というように、UniRxをインポート後では、前の例外を吐いたコードと全く同じでも、ちゃんとメインスレッドに戻してくれるようになります。

Coroutine is awaitable

UniRxを入れることで追加される機能はそれだけではなく、更に普通のコルーチンもawait可能な仕組みを裏側で仕込んでいます。これにより

async Task CoroutineBridge()
{
    Debug.Log("start www await");
    var www = await new WWW("https://unity3d.com");
    Debug.Log(www.text);
    await CustomCoroutine();
    Debug.Log("await after 3 seconds");
}
 
IEnumerator CustomCoroutine()
{
    Debug.Log("start wait 3 seconds");
    yield return new WaitForSeconds(3);
    Debug.Log("end 3 seconds");
}

といったように、WWWとかIEnumeratorを直接awaitすることが可能になります。これはUniRx側で用意した仕組みによるものなので、普通では(現状は)できません。

勿論(?)IObservableもawait可能になっています。

async Task AwaitObservable()
{
    Debug.Log("start await observable");
    await Observable.NextFrame();  // like yield return null
    await Observable.TimerFrame(5); // await 5 frame
    try
    {
        // ObservableWWW promote exception when await(difference in await WWW)
        var result = await ObservableWWW.Get("https://404.com");
        Debug.Log(result);
    }
    catch (WWWErrorException ex)
    {
        Debug.LogError(ex.ToString());
    }
    Debug.Log("end await observable");
}

ObservableWWWを使うと例外はちゃんとtry-catchのほうに投げてくれるようになって、より自然に、簡単に扱えるようになります。

まとめ

思ったよりも、普通に使えて、普通に統合できるな、という印象があります < async/await。コルーチンで扱うよりも自然で、より強力なので、非同期を扱うのに適したシチュエーションではこっちのほうが良いのは間違いないはずです。Rxとの住み分けですが、基本的に非同期が対象ならばasync/awaitのほうが良いです。が、今回見ていただいたようにIObservableはawaitableなので、コードがRxになっているならば、現在のコードから自然にasync/awaitベースにソフトに移行することが可能でしょう。

Unityが今後、標準でSynchronizationContextを入れてくるのか、コルーチン対応クラスをawait対応にさせてくるのか、などはちょっと分かりません。分かりませんが、UniRxならば、その対応がずっと後のことになるとしても、今すぐ問題なく使うことが出来ますし、その可能性を今すぐ感じ取ることが可能なので、ぜひとも試してみてください!

余談

UniRxがAssetStore STAFFPICKに選ばれましたー。

うーん、嬉しい。

過去に制作した30のライブラリから見るC#コーディングテクニックと個人OSSの原理原則

という(サブ)タイトルで、.NET Fringe Japan 2016で発表してきました。ニコ生では7時間目ぐらいから。

What, Why, How Create OSS Libraries - 過去に制作した30のライブラリから見るC#コーディングテクニックと個人OSSの原理原則 from Yoshifumi Kawai

リンク集はこちら。

作り続けることで確実にイディオムが身についていくことと、それの発展や組み合わせによって、より大きなことが出来るようになっていくんじゃないかと思います。発想も、手札が多ければ多いほど、よりよくやれるということが分かるということになりますしね。とはいえ、どうしても発想のベースは自分の手札からになっていくので、時々は異なるものへのチャレンジを意識して行わないとなー、ってとこですね。今回のスライドでも、幾つかはやったことないことを勉強のため、みたいなのがありました。Unity周りは仕事で始めたことですけれど、今は自分の中でも重要な柱です。

C#以外をやりたい、ってのは全然思わないんですが(言語の学習も悪くはないですが、それよりなにか作ったほうが100億倍良いのでは)、今猛烈に足りない/かつやりたい、のはグラフィック関係ですねー。自分で一本メガデモを作れるようになりたいってのは、ずっと昔から思っていることで、かつ、今もできていないことなので近いどこかでチャレンジしたいです。

Photon Server Deep Dive - PhotonWireの実装から見つめるPhotonServerの基礎と応用

本題と関係ない連絡ですが、UniRx 5.4.1出しました。更新内容は主にUnity 5.5 Beta対応です(不幸にもそのままだとコンパイルエラーが出てしまっていたのだ!)。LINQ to GameObject 2.2もついでに出てます。こちらは最適化を更に進めたのと、Descendants(descendIntoChildren)というベンリメソッド(子孫要素への探索時に条件で打ち切る)の追加です。どちらも便利なので是非。

と、いうわけかで、昨日、GMO Photon 運営事務局さん開催のPhoton勉強会にてPhoton Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用というタイトルで話してきました。

Photon Server Deep Dive - PhotonWireの実装から見つめるPhotonServerの基礎と応用 from Yoshifumi Kawai

Deep Diveなのか入門なのか微妙なところに落ち着いてしまいはしたのですけれど、他の通信ライブラリ候補との比較含めPhotonの検討理由、PhotonServerの真っ白な基本的なところ、PhotonWireの優れているところ、黒騎士と白の魔王で予定している構成、などなどを一通り紹介できる内容になったのではかと思います。

PhotonWireの細かい話はPhotonWire - Photon Server + Unityによる型付き非同期RPCフレームワークと、実装の(Photonと関係ないC#的な)細かい話は実例からみるC#でのメタプログラミング用法集のほうが詳しいです。おうふ。より詳細を話すつもりが、逆に表面的になってしまった。反省。

ZeroFormatter

一番反響があったのは、Photonよりも、むしろスライド53pから少し説明しているZeroFormatter(仮称)という、私が製作中の無限大に速い新シリアライザ/フォーマットの話でした。Oh……。

まぁ実際、(Unityに限らずですが特にUnityで)かなり使えるシリアライザにするつもりなので乞うご期待。JsonUtility、いいんですけど、制約が強すぎるんですよね、特にオブジェクトをデシリアライズする際に、nullが0埋めされたクラスに変換されちゃうのがかなりヤバかったりなので、汎用フォーマットとしては使いにくいのではないかな、というところはあります。速いんですけどねえ。また、FlatBuffersはAPIがヤバいので検討する価値もないと思ってます。あれはアプリケーションの層で実用に使うのは無理。

というわけで、絶妙にイイトコドリを目指してますので、乞うご期待。出来上がったらGitHubやUnityのAssetStoreに投下しますので人柱募集です。

LINQ to GameObject 2.1 - 手書き列挙子による性能向上と追加系をより使いやすく

(前回の1.3から)1年ぶりの更新です!2.0は諸事情でスキップされました。アセットストアには出したんですが内容的にもう少しやりたかったのでなかったこと扱いで。LINQ to GameObject自体の説明はVer 1.0リリース時のブログLINQ to GameObjectによるUnityでのLINQの活用を参照ください。

今回はパフォーマンスチューニングを徹底的にやりました。というのも以前の素朴な実装は、素朴な通りの性能で、いいとか悪いとかじゃなく素朴なので、やるのならいっそギチギチにやってみたらどうかな、と。性能面でここまでやってるものは絶対にないはず。

もう一つは追加系をより使いやすく。のためにガッツリと破壊的変更を入れています。破壊的変更が入った理由は、使いにくかったからです。うぇぇ……。使いにくいポイントは概ね分かっていたし、プルリク等も貰っていたのですが、API的にイマイチなもので乗り気になれず、かといってAPIを維持しているとオーバーロードの解決などの問題でうまく処理できなくて、モニョモニョしている間に一年が経ってしまった。互換性は残したくはあったんですが、使いにくいままであったり、微妙なオーバーロードの追加とかで解決するよりは良いかな、と。いう決断です。

Traverse系

APIはほとんど変わってないです(但しnameでフィルターかけるオーバーロードは消しました、HTMLやXMLと違って名前でのフィルタの重要性がかなり低いので、むしろないほうがいいかな、と)。

ヒエラルキーをツリーに見立てて、「軸」の概念を元にして、必要となる全方向での列挙を満たしています。今回、コードを劇的に書き換えたパフォーマンスチューニングを施しました。一点目は、yield returnによるコードを、全部手書きの構造体の列挙子に書き換えてます。これにより列挙に伴うゴミ発生が理想的にはなくなっています、理想的には:)

残念ながら、そのままforeachに流すと C#のGCゴミとUnity(5.5)のコンパイラアップデートによるListのforeach問題解決について によりboxingが発生しますが(ゴミ化)、それでも構造体のサイズや再帰的に処理される場合での内部処理は気を配っているので(特にDescendantsはエクストリームにチューニングしたコードに変えた(再帰を特化Stackで置き換えたり……))、以前よりも良くなっているのは間違いないです。

ちなみに、基本的にはmutableなstructは避けたほうがいいです。Enumeratorはまさにそれで、実装にも注意が必要なら、利用にも注意を要するため(これはList<T>.Enumeratorも同様で、直接触ろうとすると罠にはまるケースが出てくる)なんでもかんでもstructで、というのは止めたほうがいいでしょう、どうしてもということでなければ原則やらないほうがいい事案です。struct enumeratorを返すテクニック自体は今は亡きXNAでも使われていたので(EffectPassCollectionやModelMeshCollectionなど各種コレクションがstruct Enumeratorを返す)、まぁ最終テクニックとしては有効(但し現状Unityではどうせforeachではボックス化されるのでそこまで有効ではないので、基本やらなくていいでしょう)

LINQで繋げたら、当然普通にLINQの消費フローに入るので、そんな意味ないんですけどね!というだけなのもアレなので、改善二点目、頻出パターンについて特化した最適化を入れてます。(+ OfComponent) + First, FirstOrDefault, ToArray に関しては通常のLINQではなく、この構造体Enumeratorに特化した呼び出しをするため、所謂LINQで想像する性能劣化を受けません。社内調べによると、割と FirstOrDefault や ToArray が直接接続されてる場合が多いので、それだけでも6~7割はカバーできているのではないかな、と。

更に三点目、ToArrayNonAllocというメモリ節約/GC防止メソッドが追加されています(IEnumerable<T>にも生やしてあるのでLINQ to GameObject関係ないシーンでも使えないこともない)

GameObject[] array = new GameObject[0];
 
// 毎フレーム走査していても余計なメモリ確保はしない!
void Update()
{
    var size = origin.Children().ToArrayNonAlloc(ref array);
    for (int i = 0; i < size; i++)
    {
        var element = array[i];
    }
}

Physics.RaycastNonAllocやGetComponentsInChildren[T](List[T] results) のようなものですね。どうしても走査頻度が高くて、という場合には使えるんじゃないかと思います。まぁ、Find系は極力使わないように、というのと同じ話で、走査系を頻繁にやること自体が全然よくはないのですけれど。

また、ToArray/ToArrayNonAlloc/Destroyには(Func<GameObject, T> selector), (Func<GameObject, bool> filter), (Func<GameObject, bool> filter, Func<GameObject, T> selector), (Func<GameObject, TState> let, Func<TState, bool> filter, Func<TState, T> selector) といった、Where().Select().ToArray() のような割とよくある状況に対する最適化オーバーロードを入れてます。

この辺を活用してもらえば、単純にインラインで自前実装するよりも、むしろ速い/効率的なことのほうが多いでしょう。

特化したものを速くなるのはある種当たり前で、しかしそうするとメソッドが雪だるま式に増えるのが良くなくて、そしてLINQのいいところは合成可能なことにより特化させずとも無限の組み合わせで機能を実現できるところにある。しかし、まぁ勿論、柔軟性とパフォーマンスが幾ばくかトレードオフなのは当然の話なわけで、LINQの雰囲気を保ったまま、裏側だけ特化実装にこっそり差し替わってる。というあたりが落とし所としては良いのかな、と思ってますし、なのでそういう風に実装しました。

再帰的なイテレータの罠

Children(子要素列挙)なんかは数が大したことないので問題はそんなないんですが、Descendants(子孫要素列挙)は性能差が大きく出てきます。そして、利用頻度で言ってもDescendants系が基本多い。これのパフォーマンスを改善することは、非常に意味のあることです。さて、これはシンプルなDescendantsの実装です。

static IEnumerable<GameObject> Descendants(GameObject root)
{
    yield return root;
    foreach (Transform item in root.transform)
    {
        foreach (var child in Descendants(item.gameObject))
        {
            yield return child.gameObject;
        }
    }
}

このコードには大きな問題があります!再帰的なイテレータ、つまり foreach (var child in Descendants(item.gameObject)) は危険です。Baaaaad Practice、デス。要警戒です。これ、子孫にあるGameObjectの数だけ、イテレータ作ってます。GetEnumerator祭り!これは、LINQがどうのとかそういう次元を超えています。LINQのコストというのはメソッドチェーン分のGetEnumeratorの加算とMoveNextの連鎖による一回の呼び出しコストの増加が基本的な話で、ようするに2~3増えるという話で大したことあるといえば大したことあるし、大したことないといえば大したことない。が、さすがに要素数分だけ無駄にEnumerator作るとなったら話は別だ。ちょっとね、かなり気になるよね。

解決策は2つあります。一つはstruct enumeratorで、struct生成コストはあるもののゴミにはなりません。↑で書いたように実装済みです。

もう一点は、内部イテレーター化。イテレーターには概ね二種類、内部イテレーターと外部イテレーターがあります。外部イテレーターはforeachで使える、つまりGetEnumerator経由のもので、内部イテレーターはListのForEachなどクラスに直接生えてるもの。それぞれ利点と欠点があります。外部イテレーターの利点は柔軟性(LINQ)と言語サポート(foreach/generator)、よって基本的にはこちらを選べばOKです。欠点はパフォーマンスが内部イテレーターほど稼げない。どうしても一つシーケンスを進めるのにMoveNextとCurrentの2つのメソッド呼び出しが必要になるので。内部イテレーターの利点はパフォーマンスで、内部構造に最適化したループを回せるので、基本最速です。欠点は柔軟性がないのと、それぞれのコレクションで独自実装になること。

LINQ to GameObjectでは両方実装しています。外部イテレーターは手書きで最適化したstruct enumerator(とStackPoolと、その他諸々の仕掛け)によって、遅延実行やLINQサポートなどの柔軟性を維持したまま、パフォーマンスとGC行きのゴミを全く出さないようにしています。内部イテレーターに関してはForEachとToArray(NonAlloc)に関しては、外部イテレーター版と全く異なる実行パスを通ることにより、最速を維持します。

ところで、Unityネイティブに用意されているものがある場合は、それを使ったほうが速くなります。例えば DescendantsAndSelf().OfComponent().ToArray() は GetComponentsInChildren(includeInactive:true) に概ね等しく(一つのオブジェクトに複数コンポーネントが貼り付けてある場合、LINQ to GameObjectではそれぞれのGameObjectに一つのみ、GetComponentsInChildrenは複数と、正確には挙動が異なります)、後者を使ったほうが断然速い。一応ですが、ネイティブだから常に速いとか、そういうことはなくて、ネイティブ-マネージド間の変換コストのほうが勝る場合もあります(たとえばUnityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映のような話)。けれど、この場合は、C#だけで走査すると、GameObject毎でのGetComponentが避けられません(GetComponentのコストはタダではないのだ)。なので、一発でネイティブ内でかき集めてきたほうが絶対的に速くなります。子孫を辿るだけならほとんど遜色ない、むしろ速いといっていいぐらいなので、本当にこれはGetComponentに対する処理効率の差だけですね。これだけはどうにもできませんでした。

追加系

変わってます。使い勝手的にはこっちの対応がメインです。

以前のAPIの何が不便かって、引数にGameObjectしか受け付けなかった!そして戻り値がGameObject!大抵の場合はComponentを入れてComponentを受け取りたいのに!これは酷い!いやほんと酷すぎでした……。なんでそうなってたかというと言い訳はそれなりにあって、まずGameObjectとComponentって継承階層が別のとこにいるんですよねー、のが困る。それをオーバーロードとして分けると、IEnumerableを受け取るオーバーロードが存在していたため、どうやってもうまく型が解決できなかったのだ……。

もうどうにもならなかったので、API変えてます。IEnumerableを受け取るオーバーロードはXxxRangeという名前に分離。また、基本的には<T>を返すように、そして T:UnityEngine.Object を受け取れるようにしたので、引数としてやっとMonoBehaviourなComponentを素直に流し込めるようになりましたー。万歳。継承階層が別のとこにいて困ります問題は、UnityEngine.Objectを受け取った上で、動的にGameObjectとComponentに仕分けすることで解決。

というわけで、やっと自信持って普通に使えるようになりました。単純な話なんですが、まず破壊的変更にする、ということに腰が重かったことと、それを踏まえても、うまいAPIを構築するのに手間取った。のせいでこんなに遅れてしまって、いやはや……。

その他、あとDestroyでデフォルトでヒエラルキから外さなくしました。このヒエラルキから外すというのは最低のアイディアで、配列ではなく列挙しながら(LINQ to GameObjectでやるような!)Destroyする場合に、ヒエラルキから外すせいで位置がずれて死ぬ。というのを防ぐためにToArrayでキャッシュしなければならない(無駄なオーバーヘッド!)。というしょうもない自体に陥りがちなので、やめました。わざわざ外すコストだってゼロじゃないので、二重に悪い。

まとめ

GameObjectBuilderというものがあったのですが、イラナイ子なので消しました。LINQ to XMLのFunctional Constructionを模した――ものなのですが、そういう、コピーに一生懸命なだけなのって悪趣味なんですよね。大事なのは、概念(LINQ to Tree)を対象環境(Unity)に最適化することであって、コピーすることではない。そういうの、分かっているつもりではいたのですが、やり始めるとついついやってしまうところがある。随時見切って、バッサリ切り落とせるようにならないとですね。

LINQ to GameObjectのオリジナルのデザインは2014/10/28だったんですが、その頃は今よりは全然遥かにUnityへの習熟度、知識が欠けていたなぁ、というのを改めて痛感しました。思い上がる、ということはないですが、環境への理解力が足らないとどこかイマイチなものになってしまうわけで、C#云々抜きに、常にUnityに真摯に向き合ってかないとダメですね。実際問題、愛情を持って突き詰めて考えられないと、本当の理想のところまでは行けない。小手先の知識だけで処理したようなライブラリは、まぁ使いたくないですねえ、そういうの実際どうしてもどこか独りよがりのしょうもないものになってしまうので。

LINQは遅い/GCキツくなるというのは絶対的な事実ではあるのですけれど、極力書き味を失わないようにしつつ、6, 7割ぐらいのシチュエーションには特化した最適化を施し、何も考えずともむしろ普通に書くよりも速くなる。それ以外のシチュエーションでも、速さを意識した使い方をすれば、やはり普通に書くよりも速くなる。という、私的には理想的かな、というところで表現できたので、是非是非、機能を気にする人も、性能を気にする人も使ってみてください。どちらも満たせるものになっているはずです。

ところでしつこいですが、9/13にPhoton勉強会で「Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用」というタイトルで話しますので、Photon興味ある人も、そうでなくてもUniRx興味ある人もどうぞ。LINQ to GameObject、或いはUnityとLINQについての話は、さすがにあんま関係ないのでセッション内容には含まれませんが懇親会ででも掴まえてもらえば何でも答えます。

C#のGCゴミとUnity(5.5)のコンパイラアップデートによるListのforeach問題解決について

UnityにおいてList<T>のforeachは厳禁という定説から幾数年。しかしなんと現在Unityが取組中のコンパイラアップデートによって解決されるのだ!ついに!というわけで、実際どういう問題があって、どのように解決されるのかを詳しく見ていきます。

現状でのArrayのforeachとListのforeach

まずは現状確認。を、Unityのプロファイラで見てみます。以下の様なコードを書いて計測すると……。

var array = new int[] { 1, 2, 3, 4, 5 };
var list = new List<int> { 1, 2, 3, 4, 5 };
 
// ボタンを叩いて計測開始
button.OnClickAsObservable().Subscribe(_ =>
{
    Profiler.BeginSample("GCAllocCheck:Array");
    foreach (var item in array) { }
    Profiler.EndSample();
 
    Profiler.BeginSample("GCAllocCheck:List");
    foreach (var item in list) { }
    Profiler.EndSample();
 
    // プロファイラでそこ見たいのでサッと止める。
    Observable.NextFrame(FrameCountType.EndOfFrame).Subscribe(__ =>
    {
        EditorApplication.isPaused = true;
    });
});

image

Unityのプロファイラは使いやすくて便利。というのはともかく、なるほどListは40B消費している(注:Unity上でコンパイラした時のみの話で、普通のC#アプリなどでは0Bになります。詳しくは後述)。おうふ……。ともあれ、なぜListのforeachでは40Bの消費があるのか。ってところですよね。foreach、つまりGetEnumeratorのせいに違いない!というのは、半分合ってて半分間違ってます。つまり100%間違ってます。

GetEnumeratorとforeach

foreachはコンパイラによってGetEnumerator経由のコードに展開されます。

// このコードは
foreach(var item in list)
{
}
 
// こう展開される
using (var e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        var item = e.Current;
    }
}

GetEnumerator、つまり IEnumerator<T> はクラスなので、ヒープに突っ込まれてるに違いない。はい。いえ。だったらArrayだって突っ込まれてるはずじゃないですかー?

// こんなコードを動かしてみると
 
Profiler.BeginSample("GCAllocCheck:Array.GetEnumerator");
array.GetEnumerator();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:List.GetEnumerator");
list.GetEnumerator();
Profiler.EndSample();

image

そう、むしろArrayは32B確保していてListはむしろ0なのだ。どっちも直感的には変てこ。

配列とforeachの最適化

配列をforeachで回すとコンパイラが、forループに展開します。

// このコードは
foreach (var item in array)
{
 
}
 
// こうなる
for (int i = 0; i < array.Length; i++)
{
    var item = array[i];
}

ちなみに配列のループを回すときは明確にLengthを使うと良いです。というのも、配列の境界チェック(自動で入る)が実行時に消せます。

// こっちよりも
var len = array.Length;
for (int i = 0; i < len; i++)
{
    var item = array[i];
}
 
//  こっちのほうが速い
for (int i = 0; i < array.Length; i++)
{
    var item = array[i];
}

詳しくはArray Bounds Check Elimination in the CLRをどうぞ。ようするに基本的には配列はforeachで回しておけばおk、indexを別途使う場合があるなら、Lengthで回すことを心がけるとベター。というところでしょうか。(もっというと配列の要素は構造体であると、更にベターなパフォーマンスになります。また、配列は色々特別なので、配列 vs Listで回す速度を比較すれば配列のほうがベタベターです)

List<T>のGetEnumeratorへの最適化

list.GetEnumeratorが0Bの理由は、ここにクラスライブラリ側で最適化が入っているからです。と、いうのも、List<T>.GetEnumeratorの戻り値はIEnumerator<T>ではなくて、List<T>.Enumeratorという構造体になっています。そう、特化して用意された素敵構造体なのでGCゴミ行きしないのだ。なので、これをわざとらしくtry-finallyを使ったコードで回してみると

Profiler.BeginSample("GCAllocCheck:HandConsumeEnumerator");
 
var e = list.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        var item = e.Current;
    }
}
finally
{
    e.Dispose();
}
 
Profiler.EndSample();

image

0Bです。そう、理屈的にはforeachでも問題ないはずなんですが……。ここでちゃんと正しくforeachで「展開された」後のコードを書いてみると

using (var e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        var item = e.Current;
    }
}

image

40B。なんとなくわかってきました!?

using展開のコンパイラバグ

「List<T>をforeachで回すとGCゴミが出るのはUnityのコンパイラが古いせいでバグッてるから」というのが良く知られている話ですが、より正しい理解に変えると、「構造体のIDisposableに対するusingの展開結果が最適化されていない(仕様に基づいていない)」ということになります。この辺の話はECMA-334 C# Language Specificationにも乗っているので、C#コンパイラの仕様に対するバグと言ってしまうのは全然良いのかな?

どういうことかというと、現状のUnityのコンパイラはこういうコードになります。

var e = list.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        var item = e.Current;
    }
}
finally
{
    var d = (IDisposable)e; // ここでBoxing
    d.Dispose(); // 本来は直接 e.Dispose() というコードでなければならない
}

そう、全体的に良い感じなのに、最後の最後、Disposeする時にIDisposableにボックス化してしまうので、そこでGCゴミが発生するというのが結論です。そして、これは最新のmonoコンパイラなどでは直っています、というか2010年の時点で直ってます。どんだけ古いねん、Unityのコンパイラ……。

40Bの出処

ゴミ発生箇所は分かったけれど、せっかくなのでもう少し。サイズが40Bの根拠はなんなの?というところについて。まずは色々なもののサイズを見ていきましょうー。

// こんなのも用意した上で
struct EmptyStruct
{
}
 
struct RefStruct
{
    public object o;
}
 
class BigClass
{
    public long X;
    public long Y;
    public long Z;
}
---
 
// 色々チェックしてみる
Profiler.BeginSample("GCAllocCheck:object");
var _0 = new object();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:class");
var _1 = new BigClass();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:int");
var _2 = 99;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:int.boxing");
object _3 = 99;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:emptyStruct");
var _4 = new EmptyStruct();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:emptyStruct.boxing");
object _5 = new EmptyStruct();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:bool.boxing");
object _6 = true;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:float.boxing");
object _7 = 0.1f;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:double.boxing");
object _8 = 0.1;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:refStruct.boxing");
object _9 = new RefStruct();
Profiler.EndSample();

image

なるほどなるほど。当たり前ですがstructのままのは0B。EmptyStructやboolなど最小1バイトのboxingは17B(ほえ?)、int(4バイト)が20Bでdouble(8バイト)や参照を一個持たせた(IntPtr - 64bit環境において8バイト)構造体が24B。classにlongを3つめたのが40B。そしてobjectが16B。つまり。つまり、最小が16Bで、そこからフィールドのそれぞれの要素のサイズが加算されるということです。

この16 bytesがどこから来ているかというと、オブジェクトのヘッダです。ああ、なるほどそういう……。

さて、これを踏まえてListのEnumeratorのフィールドを見てみると

public struct Enumerator : IEnumerator, IDisposable, IEnumerator<T>
{
	private List<T> l;
	private int next;
	private int ver;
	private T current;

ヘッダ16B + IntPtrの8B + intの4B + intの4B + Tがintの場合は4B = 36B。40じゃないじゃん、ってところは、32以降は8Bずつ埋まってくっぽ、実質33Bだと40B, 41Bだと48Bという感じ。といったところから40Bの消費になっていたということですね!

Experimental Scripting Previews

ついにコンパイラアップデートのPreviewがやってきた!Experimental Scripting Previewsにて、コンパイラのアップデートプロジェクトも始まっています。そして今のところ5.3.5p8-csharp-compiler-upgradeが配られています。

というわけで早速、冒頭の配列とListのforeachをかけてみると……

Profiler.BeginSample("GCAllocCheck:Array");
foreach (var item in array) { }
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:List");
foreach (var item in list) { }
Profiler.EndSample();

image

やった!これで問題nothingですね!(実際は計測時は初回にListのほうに32B取られててあれれ?となったんですが、コンパイル後のIL見ても正常だし、まぁ二回以降叩いたのは↑画像の通りになったので、よしとしておこ……)

まとめ

で、現状はList<T>の列挙はどうすればいいのか、というと、まぁforでindexerでアクセスが安心の鉄板ではある。ForEachが内部配列に直接アクセスされるので速い説はなくはないですが、ForEachだとラムダ式のキャプチャに気を使わないと逆効果なので(詳しくはUnityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方)、基本的には普通にforがいいと思います(なお、キャプチャのないように気を使えば、ForEachのほうが速度を稼げる余地はあります。理論上、正常になったforeachよりも良い場合があるため)

理想的にはforeachであるべきだし、改革の時はまもなく!(5.5に↑のコンパイラアップグレードは入るっぽいですよ)。ちなみに、あくまでコンパイラのアップグレードなだけで、フレームワークのアップデートや言語バージョンのアップデートは今は含まれてはいない。段階的にやっていく話だと思うので、とりあえずはコンパイラがより良くなる、というだけでも良いと思ってます。というか全然良いです。素晴らしい。

UniRx 5.4.0 - Unity 5.4対応とまだまだ最適化

UniRx 5.4.0をリリースしました!ちょうどUnity 5.4もリリースされたので、5.4向けの修正(Warning取り除いただけですが)を出せて良かった。というわけで5.4正式対応です。リリースは前回が5月だったので3ヶ月ぶりです。5.2 -> 5.3も3ヶ月だったので、今のとこ3ヶ月スパンになってますが偶然です。

何が変わったのかというと

Add: Observable.FrameInterval
Add: Observable.FrameTimeInterval
Add: Observable.BatchFrame
Add: Observable.Debug(under UniRx.Diagnostics namespace)
Add: ObservableParticleTrigger and OnParticleCollisionAsObservable, OnParticleTriggerAsObservabl(after Unity 5.4) extension methods
Add: UniRx.AsyncReactiveCommand
Add: ReactiveCommand.BindToOnClick, `IObservable<bool>.BindToButtonOnClick`
Add: UniRx.Toolkit.ObjectPool, AsyncObjectPool
Add: UniRx.AsyncMessageBroker, asynchronous variation of MessageBroker
Add: ObserveEveryValueChanged(IEqualityComparer) overload
Add: `Observable.FromCoroutine(Func<CancellationToken, IEnumerator>)` overload
Add: ObservableYieldInstruction.IsDone property
Add: IPresenter.ForceInitialize(object argument)
Improvement: Where().Select(), Select().Where() peformance was optimized that combine funcs at internal
Improvement: MicroCoroutine performance was optimized that prevent refresh spike
Improvement: Observable.Return performance was optimized that reduced memory cost
Improvement: Observable.Return(bool) was optimzied perofmrance that allocate zero memory
Improvement: Observable.ReturnUnit was optimzied perofmrance that allocate zero memory
Improvement: Observable.Empty was optimzied perofmrance that allocate zero memory
Improvement: Observable.Never was optimzied perofmrance that allocate zero memory
Improvement: Observable.DelayFrame performance was optimized
Improvement: UnityEqualityComparer.GetDefault peformance was optimized
Improvement: AddTo(gameObject) dispose when ObservableTrigger is not activated
Improvement: AddTo(gameObject/component) performance was optimized by use inner CompositeDisposable of ObservableDestroyTrigger
Improvement: `FromCoroutine<T>(Func<IObserver<T>, IEnumerator>)` stops coroutine when subscription was disposed
Improvement: ReactiveCollection, ReactiveDictionary implements dispose pattern
Fix: ToYieldInstruction throws exception on MoveNext when reThrowOnError and has error 
Fix: ObserveEveryValueChanged publish value immediately(this is degraded from UniRx 5.3)
Fix: Prevent warning on Unity 5.4 at ObservableMonoBehaviour/TypedMonoBehaviour.OnLevelWasLoaded
Fix: Remove indexer.set of IReadOnlyReactiveDictionary
Breaking Changes: Does not guaranty MicroCoroutine action on same frame
Breaking Changes: UniRx.Diagnostics.LogEntry was changed from class to struct for performance improvement

相変わらずへっぽこな英語はおいといてもらえるとして、基本的にはパフォーマンス改善、です。

前回紹介したMicroCoroutineを改良して、配列をお掃除しながら走査する(かつ配列走査速度は極力最高速を維持する)ようになったので、より安定感もましたかな、と。その他メモリ確保しないで済みそうなものは徹底的に確保しないようになど、しつっこく性能改善に努めました。あと新規実装オペレータに関しては性能に対する執拗度がかなり上がっていて、今回でいうとBatchFrameはギチギチに最適化した実装です。既存オペレータも実装甘いものも残ってはいるので、見直せるものは見なおしてみたいですねえ。

また、9/13日にPhoton勉強会【Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用、ほか】で登壇するので、PhotonWireではUniRxもクライアント側でかなり使っているので、その辺もちょっと話したいなと思っていますので、Photonに興味ある方もない方も是非是非。Photon固有の話も勿論しますが、普通にUnityとリアルタイム通信エンジンについての考えや、UniRx固有の話なども含めていきますので。

Debug

Debugという直球な名前のオペレータが追加されました。標準では有効化されていなくて、UniRx.Diagnosticsというマイナーな名前空間をusingするようで使えるようになります。実際どんな効果が得られるのかというと

using UniRx.Diagnostics;
 
---
 
// [DebugDump, Normal]OnSubscribe
// [DebugDump, Normal]OnNext(1)
// [DebugDump, Normal]OnNext(10)
// [DebugDump, Normal]OnCompleted()
{
    var subject = new Subject<int>();
 
    subject.Debug("DebugDump, Normal").Subscribe();
 
    subject.OnNext(1);
    subject.OnNext(10);
    subject.OnCompleted();
}
 
// [DebugDump, Cancel]OnSubscribe
// [DebugDump, Cancel]OnNext(1)
// [DebugDump, Cancel]OnCancel
{
    var subject = new Subject<int>();
 
    var d = subject.Debug("DebugDump, Cancel").Subscribe();
 
    subject.OnNext(1);
    d.Dispose();
}
 
// [DebugDump, Error]OnSubscribe
// [DebugDump, Error]OnNext(1)
// [DebugDump, Error]OnError(System.Exception)
{
    var subject = new Subject<int>();
 
    subject.Debug("DebugDump, Error").Subscribe();
 
    subject.OnNext(1);
    subject.OnError(new Exception());
}

シーケンス内で検出可能なアクション(OnNext, OnError, OnCompleted, OnSubscribe, OnCancel)が全てコンソールに出力されます。よくあるのが、何か値が流れてこなくなったんだけど→どこかで誰かがDispose済み(OnCompleted)とか、OnCompletedが実は呼ばれてたとかが見えるようになります。

超絶ベンリな可視化!ってほどではないんですが、こんなものがあるだけでも、Rxで困ったときのデバッグの足しにはなるかなー、と。

BatchFrame

BatchFrameは特定タイミング後(例えばEndOfFrameまでコマンドまとめるとか)にまとめて発火するという、Buffer(Frame)のバリエーションみたいなものです。都度処理ではなくてまとめてから発火というのは、パフォーマンス的に有利になるケースが多いので、そのための仕組みです。Bufferでも代用できなくもなかったのですが、Bufferとは、タイマーの回るタイミングがBufferが空の時にスタートして、出力したら止まるというのが大きな違いですね。その挙動に合わせて最適化されています。

// BatchFrame特定タイミング後にまとめられて発火
// デフォルトは0フレーム, EndOfFrameのタイミング
var s1 = new Subject<Unit>();
var s2 = new Subject<Unit>();
 
Observable.Merge(s1, s2)
    .BatchFrame()
    .Subscribe(_ => Debug.Log(Time.frameCount));
 
Debug.Log("Before BatchFrame:" + Time.frameCount);
 
s1.OnNext(Unit.Default);
s2.OnNext(Unit.Default);

実装的には、まとめる&発火のTimerはコルーチンで待つようにしているのですが、今回はそのIEnumeratorを手実装して、適宜Resetかけて再利用することで、パイプライン構築後は一切の追加メモリ消費がない状態にしてます。

Optimize Combination

オペレータの組み合わせには、幾つかメジャーなものがあります。特に代表的なのはWhere().Select()でしょう。これはリスト内包表記などでも固有記法として存在するように、フィルタして射影。よくありすぎるパターンです。また、Where().Where()などのフィルタの連打やSelect().Select()などの射影の連打、そして射影してフィルタSelect().Where()などもよくみかけます(特にWhere(x => x != null)みたいなのは頻出すぎる!)。これらは、内部的に一つのオペレータとして最適化した合成が可能です。

// Select().Select()
onNext(selector1(selector2(x)));
 
// Where().Where()
if(predicate1(x) && predicate2(x))
{
    onNext(x);
}
 
// Where().Select()
if(predicate(x))
{
    onNext(selector(x));
}
 
// Select().Where()
var v = selector(x);
if(predicate(v))
{
    onNext(v);
}

と、いうわけで、今回からそれらの結合を検出した場合に、内部的には自動的にデリゲートをまとめた一つのオペレータに変換して返すようになっています。

MessageBroker, AsyncMessageBroker

MessageBrokerはRxベースのインメモリPubSubです。AndroidでOttoからRxJavaへの移行ガイドのような記事があるように、PubSubをRxベースで作るのは珍しいことではなく、それのUniRx版となってます。

UniRxのMessageBrokerは「型」でグルーピングされて分配される仕組みにしています。

// こんな型があるとして
public class TestArgs
{
    public int Value { get; set; }
}
 
---
 
// Subscribe message on global-scope.
MessageBroker.Default.Receive<TestArgs>().Subscribe(x => UnityEngine.Debug.Log(x));
 
// Publish message
MessageBroker.Default.Publish(new TestArgs { Value = 1000 });
 
// AsyncMessageBroker is variation of MessageBroker, can await Publish call.
 
AsyncMessageBroker.Default.Subscribe<TestArgs>(x =>
{
    // show after 3 seconds.
    return Observable.Timer(TimeSpan.FromSeconds(3))
        .ForEachAsync(_ =>
        {
            UnityEngine.Debug.Log(x);
        });
});
 
AsyncMessageBroker.Default.PublishAsync(new TestArgs { Value = 3000 })
    .Subscribe(_ =>
    {
        UnityEngine.Debug.Log("called all subscriber completed");
    });

AsyncMessageBrokerはMessageBrokerの非同期のバリエーションで、Publish時に全てのSubscriberに届いて完了したことを待つことができます。例えばアニメーション発行をPublishで投げて、Subscribe側ではそれの完了を単一のObservableで返す、Publish側はObservableになっているので、全ての完了を待ってSubscribe可能。みたいな。文字だけだとちょっと分かりにくいですが、使ってみれば結構簡単です。

UniRx.Toolkit.ObjectPool/AsyncObjectPool

UniRx.Toolkit名前空間は、本体とはあんま関係ないけれど、Rx的にベンリな小物置き場という感じのイメージでたまに増やすかもしれません。こういうのはあまり本体に置くべき「ではない」とも思っているのですが、Rxの内部を考慮した最適化を施したコードを書くのはそこそこ難易度が高いので、実用的なサンプル、のような意味合いも込めて、名前空間を隔離したうえで用意していってもいいのかな、と思いました。

というわけで、最初の追加はObjectPoolです。ObjectPoolはどこまで機能を持たせ、どこまで汎用的で、どこまで特化させるべきかという範囲がかなり広くて、実装難易度が高いわけではないですが、好みのものに仕上げるのは難しいところです。なのでまぁプロジェクト毎に作りゃあいいじゃん、と思いつつもそれはそれで面倒だしねー、の微妙なラインなのでちょっと考えつくも入れてみました。

// こんなクラスがあるとして
public class Foobar : MonoBehaviour
{
    public IObservable<Unit> ActionAsync()
    {
        // heavy, heavy, action...
        return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
    }
}
 
// それ専用のPoolを<T>で作る
public class FoobarPool : ObjectPool<Foobar>
{
    readonly Foobar prefab;
    readonly Transform hierarchyParent;
 
    public FoobarPool(Foobar prefab, Transform hierarchyParent)
    {
        this.prefab = prefab;
        this.hierarchyParent = hierarchyParent;
    }
 
    // 基本的にはこれだけオーバーロード。
    // 初回のインスタンス化の際の処理を書く(特定のtransformに下げたりとかその他色々あるでしょふ)
    protected override Foobar CreateInstance()
    {
        var foobar = GameObject.Instantiate<Foobar>(prefab);
        foobar.transform.SetParent(hierarchyParent);
 
        return foobar;
    }
 
    // 他カスタマイズする際はOnBeforeRent, OnBeforeReturn, OnClearをオーバーロードすればおk
    // デフォルトでは OnBeforeRent = SetActive(true), OnBeforeReturn = SetActive(false) が実行されます
 
    // protected override void OnBeforeRent(Foobar instance)
    // protected override void OnBeforeReturn(Foobar instance)
    // protected override void OnClear(Foobar instance)
}
 
public class Presenter : MonoBehaviour
{
    FoobarPool pool = null;
 
    public Foobar prefab;
    public Button rentButton;
 
    void Start()
    {
        pool = new FoobarPool(prefab, this.transform);
 
        rentButton.OnClickAsObservable().Subscribe(_ =>
        {
            // プールから借りて
            var foobar = pool.Rent();
            foobar.ActionAsync().Subscribe(__ =>
            {
                // 終わったらマニュアルで返す
                pool.Return(foobar);
            });
        });
    }
}

基本的に手動で返しますし、貸し借りの型には何の手も入ってません!Rent後のトラッキングは一切されてなくて、手でReturnしろ、と。まあ、9割のシチュエーションでそんなんでいいと思うんですよね。賢くやろうとすると基底クラスがばら撒かれることになって、あまり良い兆候とは言えません。パフォーマンス的にも複雑性が増す分、どんどん下がっていきますし。

どこがRxなのかというと、PreloadAsyncというメソッドが用意されていて、事前にプールを広げておくことができます。フリーズを避けるために毎フレームx個ずつ、みたいな指定が可能になっているので、その完了がRxで待機可能ってとこがRxなとこです。

それと同期版の他に非同期版も用意されていて、それは CreateInstance/Rent が非同期になってます。

MessageBrokerと同じくAsyncとそうでないのが分かれているのは、Asyncに統一すべき「ではない」から。統一自体は可能で、というのも同期はObservable.Returnでラップすることで非同期と同じインターフェイスで扱えるから。そのこと自体はいいんですが、パフォーマンス上のペナルティと、そもそもの扱いづらさ(さすがにTのほうがIObservable[T]より遙かに扱いやすい!)を抱えます。

sync over asyncは、UniRx的にはバッドプラクティスになるかなあ。なので、同期版と非同期版とは、あえて分けて用意する。使い分ける。使う場合は極力同期に寄せる。ほうがいいんじゃないかな、というのが最近の見解です。

なお、Rent, Returnというメソッド名はdotnet/corefxのSystem.Buffersから取っています。

AsyncReactiveCommand

というわけでこちらもsync/asyncの別分けパターンで非同期版のReactiveCommandです。ReactiveCommandは何がベンリなのか分からないって話なのですが、実はこっちのAsyncReactiveCommandはかなりベンリです!

public class Presenter : MonoBehaviour
{
    public UnityEngine.UI.Button button;
 
    void Start()
    {
        var command = new AsyncReactiveCommand();
 
        command.Subscribe(_ =>
        {
            // heavy, heavy, heavy method....
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
        });
 
        // after clicked, button shows disable for 3 seconds
        command.BindTo(button);
 
        // Note:shortcut extension, bind aync onclick directly
        button.BindToOnClick(_ =>
        {
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
        });
    }
}

interactableの状態をコード実行中、というかつまりIO<T>が返されるまでfalseにします。連打防止でThrottleFirstがよく使われますが、それをより正確にコントロールしたり、また、引数にIReactiveProperty[bool]を渡せて、それを複数のAsyncReactiveCommandで共有することで、特定のボタンを実行中は他のボタンも実行できない、のような実行可否のグルーピングが可能になります(例えばグローバルでUI用に一個持っておけば、ゲーム中でUIは単一の実行しか許可されない、的なことが可能になる)

PresenterBase再考

PresenterBase、Obsoleteはつけてないのですけれど、GitHub上のReadMeで非推奨の明言を入れました。賢い基底クラスは悪。なのです。POCO。それはUnityにおいても何事においても例外ではない。その原則からするとPresenterBaseは賢すぎたのでナシ of the Year。動きはする、動きはするんですが……。

Model-View-Presenterパターン自体の否定ではなくて(それ自体は機能するとは思っています、ただし関心がModelにばかり向きがちですが、Viewは何か、Presenterは何か、についてもきちんと向き合わないとPresenterが奇形化するかなー、というのは実感としてある。ViewであるものをPresenterとして表現してアレゲになる、とか)、PresenterBaseというフレームワークのミスかな、とは。です。

とりあえずいったん初期化順序が気になるシーンは手でInitializeメソッド立てて、それをAwake, Startの代わりにして、呼ばせる。いじょ。みたいな素朴な奴で十二分かなー、とオモッテマス。結局。メリットよりもデメリットのほうが大きすぎたかな。反省。

この辺りに関してはアイディアはあるので、形にするまで、むー、ちょっと味噌汁で顔洗って出直してきます。

まとめ

あんまり大きな機能追加はなく細々とした変化なんですが、着々と良くはなっているかな、と!

Rxに関してもバッドプラクティスを色々考えられるというか反省できる(おうふ……)ようになっては来たので、どっかでまとめておきたいですね。油断するとすぐリアクティブスパゲティ化するのはいくないところではある。強力なツールではあるんですが、やりすぎて自爆するというのは、どんなツールを使っても避けられないことではあるけれど、Rxがその傾向はかなり強くはある。

まぁ、sync over asyncはいくないです。ほんと(思うところいっぱいある)。

というわけかで繰り返しますが、9/13日にPhoton勉強会【Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用、ほか】で登壇するので、よければそちらも是非是非です。

LightNode 2 - OWINからASP.NET Coreへの移植実例

ASP.NET Core以前に.NET Coreをガン無視している昨今。というのも、.NET Coreというかようするところ最近の.NETは横、つまりクロスプラットフォームへの広がりなんですよね。それ自体は素晴らしく良いことではあるのですが、縦、つまり機能面での拡充があるのかどうかというと、あんまない気がしています。それは、クロスプラットフォームいうても基本的にはWindowsでしか現状/当分は使わないんだよなー、という私みたいな人間にとってはあまり興味を引かれるものではないのであった。

とかっていつまでも言ってるのもアレなので、とりあえずLightNode(という私の作ってるOwinで動くMicro REST Framework、ようはASP.NET Web APIみたいなやつ)をASP.NET Coreに移植してみました。アプリケーションの移植じゃなくてライブラリの移植なので、むしろ楽です。LightNodeはガチガチにOwinのみで構築していたので、ほとんど単純な置換のみでOKでした。

ASP.NET Coreで動作させるだけなら、OWIN - ASP.NET Coreのブリッジを使うという手もありますが、今回は完全にASP.NET Core向けに書き直しました。せっかくやるなら、ちゃんとしっかりしたものにしたいですしね、HttpContextのほうが望ましいのにIDictionaryなEnvironmentが露出してたりすると嫌じゃん。そんなわけでつまり、OWINに関連する部分は完全にASP.NET Core仕様に変わったので、互換性はありません。

ASP.NET Coreライブラリ開発の準備と移植手順

準備としてVisual Studio 2015 Update 3.NET Core SDKを入れればOK。が、しかしいきなり.NET Core SDKがUpdate 3が入ってねーよエラーが出てインストールできなくて泣いた。世の中厳しい。Forumによるとそういう事例多数。対応としては「DotNetCore.1.0.0-VS2015Tools.Preview2.exe SKIP_VSU_CHECK=1」で叩けば入るよって話で、そうして叩くことによってようやく準備OK。幸先は悪い。

そうして入ったらテンプレートに.NET Core系があるので、とりあえずClass Library(.NET Core)を作る。参照してるのは .NETStandard Library 1.6.0。この辺良く分からないんですが、corefx/.NET Platform Standardによると.NET 4.6.3ぐらいに相当するそうで。ふーむ、まぁASP.NET Core系がnetcoreapp 1.0で1.6と同じところらしいので、このままでOKっぽい。気がする。とりあえず。UWPとかが視野の場合はちょっと話は違うのでしょうけれど。

次にASP.NET系のライブラリをNuGetで入れる。のですが、どの参照をいれればいいのかがまず分からない:) 今回はOwin的なMiddlewareを作りたかったんですが、ここはMicrosoft.AspNetCore.Http.Abstractionsが最適のようですね。これでようやくスタートライン。

既存のLightNodeのコードを突っ込むと当然激しくコンパイルエラーが出るのでここからチマチマと直していきました。まず目につくのがリフレクション関連で、IsEnumとかTypeに生えてる判別系のメソッドが片っ端から動いてません。誰しもが通る.NET Coreの洗礼!これは、type.GetTypeInfo() によるTypeInfoのほうにIsEnumなどなどが生えてるので、ひたすらGetTypeInfoを書き加えるだけの簡単なお仕事をします。GetTypeInfoの嫌なところはSystem.Reflection名前空間への拡張メソッドとして実装されてるので、IntelliSenseに出てこなくてイラッとする率が高いこと……。まぁ、あと実際にひたすらGetTypeInfoを書きまくるのは面倒くさいので、Typeへの拡張メソッドとして GetTypeInfo().IsEnum とかコンパイルエラー出てるものだけ定義してやることで作業量低減(まぁプロパティは()を書かなきゃいけないのでアレですけど。拡張プロパティはよ)

また、Parallel.ForEach がない!これは.NETStandardには含まれてないそうなので、別途System.Threading.Tasks.ParallelをNuGetから拾ってくる。なんかこう、標準に入ってて当たり前だろ、みたいに思うものが別添えになってるの、不思議な感覚ですね。これだともはやReactive Extensionsが標準にないとかImmutable Collecitonsが標準にないとか、どうでもというか全く大したことない話に見えます。なんせParallel.ForEachすらないんだから!(ところでNuGetのVersion History見ると結構細かくアップデートされてはいるんですが、いったいなにが変わったのかRelease Note出して欲しくはある……)

AppDomain.CurrentDomain.GetAssemblies もない!対象アセンブリ内からControllerがわりのクラスを引っ張ってきたくて、読み込み済みのAssemblyからGetTypesして全部検査したい、というのをやりたいわけですが、ないんですねえ。そして実際、これの代替は今のところないらしい……(というかAppDomainが今のところない)。フレームワーク系の常套手段なのに……。Loaderがどうのこうのとか、あとASP.NET Core側で特化した何かはありそうな気配を感じなくもなかったんですが、今回はGetAssembliesじゃなくても回避可能なので(一手間ではあるんですが、外側からその対象AssemblyのTypeを渡してさえくれればAssembly拾えてGetTypesできる)、Typeを渡してもらう方式のみに制限することでとりあえず回避しました。

ここから先はASP.NET Core的なところ。 IDictionary[string, object]HttpContextに変える。そしてAppFuncRequestDelegateに変える。だけの簡単なお仕事。OWINとASP.NET Coreの差異はそれだけだし中身一緒なので、機械的に置き換えていくだけ。OWINに関してはどうだのこうだのと一悶着あったりなかったりで色々ありましたが、一番下のレイヤーで触ってる限りは、概念はほんと完全に一緒なので無駄なことは全くなかったですね。上のレイヤーで触っていても、それはそれで何も考えず置き換えられるはずなので、実際のとこOWINは良かったと思ってます。新しい、ASP.NET CoreのHttpContextは、昔のHttpContextというよりかは、OWINのEnvironmentそのものだったりしますからね。

これでコンパイル通ったので、実行確認のためASP.NET Core Web Application(.NET Core)テンプレートから新規プロジェクトを作成。ASP.NET Core的なテンプレートはもちろんEmptyで。Startupに以下のを書いて

using LightNode;
using LightNode.Server;
 
public class Startup
{
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseLightNode(typeof(Startup));
    }
}
 
public class Toriaezu : LightNodeContract
{
    public string Echo(string x)
    {
        return x;
    }
}

http://localhost:15944/Toriaezu/Echo?x=hoge にアクセスでhogeが出力される。おー、ちゃんとできてますね!当たり前っちゃあ当たり前でしょうけれど、あまりに意外にすんなり動いたので普通に感動した。いやあ、いいじゃんASP.NET Core。

Swagger Included

LightNode 1の時はSwaggerは別添えだったんですが、今回はとりあえず一緒に突っ込んじゃいました。JSON.NET使ったJSON出力とかも同梱です(というかデフォルトがそれになってます)。まぁSwaggerに関してはDependencyが増えるわけでもないしいいじゃんといえばいいじゃん、なので。いいかな、と。LightNodeのSwagger統合はビュー(HTMLとかCSSとか画像とか)がDLLに埋め込んでやってたんですが、そうしたリソースを.NET Coreで埋め込むにはどうすればいいのか。今まではPropertiesで埋めてったんですが、.NET Coreではproject.jsonに書くのが正解のようですねー。

"buildOptions": {
        "embed": [
            "Swagger/SwaggerUI/**"
        ]
}

buildOptions.embedで指定できるようで、ああ、なるほど、これはこれで知ってれば凄く楽なので、全然良いですね。良いです。いいじゃん.NET Core。

というわけでサクッとSwagger統合も果たせた。

public class Startup
{
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.Map("/api", builder =>
        {
            builder.UseLightNode(typeof(Startup));
        });
 
        app.Map("/swagger", builder =>
        {
            var xmlName = "AspNetCoreSample.xml";
            var xmlPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), xmlName);
 
            builder.UseLightNodeSwagger(new LightNode.Swagger.SwaggerOptions("AspNetCoreSample", "/api")
            {
                XmlDocumentPath = xmlPath,
                IsEmitEnumAsString = true
            });
        });
    }
}

うーん、全然いけますね、じゃあ次行こう、次。

Glimpse not Included

LightNodeのウリはSwagger統合とGlimpse統合、特にGlimpseへの診断情報表示は力を入れていて(Glimpseへのハックも含めて)他にここまでやってるフレームワークはないほどでした。ので、当然ASP.NET Coreでもやりたいわけですが、んー、そもそもGlimpseがまだASP.NET Coreに本対応してない……。2.0 Betaで一応対応してるということで、あるだけマシか?と思いきや、かなり古いもので全然動かない。というかGitHubでの開発も(1.x系も2.x系も)なんかもうほとんど動いてない……。メイン開発者2名がMicrosoftに転職ということで、ASP.NET Core対応含めてよりアクティブになるのかなー、とか思ってたら、まさかの大失速……。多分、Microsoft内では別のことやっていて、そっちが忙しくて以前よりもなお作業できなくなってるんでしょうね。しかし、うーん、残念だなあ。

ASP.NET Coreに移れない/移りたくない理由があるとしたら、このGlimpseが全然対応してないってことでしょうかねえ。Glimpse自体はほんと素晴らしいので、なんとか再生してくれればいいのですけれど。

感想

.NET CoreにせよASP.NET Coreにせよ、結構コマンド操作がフィーチャーされてて、ゆとりな私には辛いものがあるんですが、さすがに1.0、普通に書いてる限りは、Visual Studio使ってる限りは特にコマンドの必要性もなく、それなりに快適に書けますね。安定してねえー、とか不満に思うことも全然ないので、もう普通に良さそう。いや、思ってたよりも全然いい感じだった。

さて、じゃあASP.NET CoreでもLightNode使おうぜ!になるかというと、うーん、とりあえずまずは普通にASP.NET Core MVCでいいでしょう(笑)。時代がねー、ちょっと違いますからね。LightNodeも3年前ですから。まぁ、でも設計思想とか全然古くなってないというかむしろASP.NET Core MVCがようやく追いついてきたかな、ぐらいの勢いだとは思ってます!例えばASP.NET Core MVCのFilterは完全にLightNodeのフィルターと一緒ですからね。

// ASP.NET Core MVC
public class SampleAsyncActionFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // do something before the action executes
        await next();
        // do something after the action executes
    }
}
 
// LightNode
public class SampleFilterAttribute : LightNodeFilterAttribute
{
    public override async Task Invoke(OperationContext operationContext, Func<Task> next)
    {
        // do something before the action executes
        await next();
        // do something after the action executes
    }
}

Filters vs. Middlewareの話なんかも、それは3年前に全部考えきって実装し実践してますから(ホジホジ。という話なんで、まぁ全然LightNode 2もいいじゃないでしょうか。LightNodeは他に、密接に統合されたクライアント自動生成などもありますしね。かわりにMVC + Web API的な、Razorのビューを返すコントローラーとWeb APIコントローラーとの統合、みたいなのができてないのは痛み。ここ馴染ませられないのは普通に不便だということを最近良く感じてるので、ASP.NET Core MVCいいですね。いいですね。

OWINベースで書いたものの移行はそこそこすんなり行けるだろうなあ、という感触はなんとなく掴めた気がします。逆に、やっぱASP.NET MVC 5あたりからの移行は厳しそう。厳しいでしょう。どうするんでしょうね、どうしようかな、参りましたね……。

ともあれせっかくの新しい.NETの幕開けなので、もう少しポジティブに情報掴んで行こうかなー、という気にはなれたのでめでたしめでたし。

BigQueryを中心としたヴァルハラゲートのログ分析システム

というタイトルでGoogle for Mobile | Game Bootcampで発表しました。4月なので3ヶ月遅れでスライド公開です。

BigQueryを中心としたヴァルハラゲートのログ分析システム from Yoshifumi Kawai

なんかあまり上手く話せなかったな、という後悔がなんかかなり残ってます:) スライドもフォント細くて吹き出しの文字が見辛いな!とりあえず、WindowsでBigQueryなシステムとしては一つの参考例にはなるのではないかなー、と思います。第一部完。

第二部はEtwStreamへの移行と、BigQuerySinkのOSS公開かなー、というところなんですがまだまだまだまだまだ先っぽいのでアレでコレでどうして。できれば誰もが秒速でASP.NETアプリケーションのログをBigQueryに流し込める、みたいな状況にしたいのですけれどねえ、そこはまだまだ遠いかなー、ですね。そのへんの.NETのエコシステムは弱いと言わざるをえない。けれどまぁ、地道に補完していきたいと思ってます。

Japan VR Hackathonに参加し、AMD賞受賞しました

Japan VR Hackathonに参加してきました!の結果が昨日発表されまして、AMD賞(Best Graphics)を受賞しました。やったー。事前に決めた5人チームでの参加で、大賞取る!という気概でやってたので、入選できて良かったです。

今回作ったのは「Clash of Oni Online」というゲームで、HTC Vive用のVRゲームです。テーマである”日本らしさ”を(一応)イメージした(一応)和風の装い。

飛んで来る岩を

吹っ飛ばす

という、VRバッティングセンター。ViveはVRというだけじゃなくてコントローラーがあるのがいいですねー。

今回、2日間 31時間で242コミット(最初のコミットが2016/06/18 09:13で 最後のコミットが 2016/06/19 16:34)。時間制限のなかでは、ちゃんとゲームしてる(ゲーム性的にもとりあえず爽快に全部打ち倒すパターンと一球一球を狙い撃ちしないパターンを用意)し、グラフィックもまぁ見栄えがするレベルで、オンライン協力プレイも実装(ただしデモ時はオフラインモード)したのは結構頑張った。ハッカソン系参加が全員初めての割には綺麗に収められた感あります。

チーム編成と最終的な役割は

  • 私(プログラマ):プロジェクトセットアップ、敵ボス挙動、サーバーセットアップ、エフェクト発注、雑進行管理、プレゼンスライド作成、動画撮影
  • プログラマ:マルチプレイプログラム、シーン管理プログラム
  • プログラマ:Viveプログラム、エフェクト組み込み
  • プログラマ:企画、地形エディット、サウンド、アセット検索、デモ
  • テクニカルアーティスト:アセット検索、敵モーション作成、ライティング、エフェクト

という感じでした。全員ほぼViveのプログラミング経験はなし(SDK入れて雰囲気掴んだことはあります程度)

最初に入れたアセットは

  • UniRx - ないと無理
  • LINQ to GameObject - ベンリ、だけど今回は別にあまり使わず
  • PhotonWire - マルチプレイ前提なので。合わせてサーバー側プロジェクトもセットアップ
  • SteamVR Plugin - Viveがターゲットなので
  • The Lab Renderer - 使ったことなかったので結局あんま余裕なく使えなかった…

という編成から随時アセット追加追加。

タイムライン

1週間ぐらい前に参加を決める。3日前ぐらいに「2人マルチ協力プレイ」「背中合わせにして立ちまわる(時代劇にある格好いい感じのアレ)雑魚戦 + デカい鬼を撃退するボス戦というアクションゲーム」「グラフィックで魅せる」を軸にする。というのを決定。和風です、和風。コミュニケーション手段としてSlack(チャット)を前日に立てる。Unityのバージョンを5.4.0b21に決めて全員にインストールしておいてもらうように。持ち込み物としてHTC Viveを2台用意。当日、会場ついてからGitHub(リポジトリ管理)のPrivateリポジトリを立てて全員招待。

雑魚戦+ボス戦といっても、作りきれるか怪しいので(実際ボス戦で手一杯だった、そりゃそうだ)、ボス戦から先に作っていくように。最初に決めていた役割分担は

  • 私(プログラマ):マルチプレイ
  • プログラマ:ボスプログラム、プレイヤー行動プログラム
  • プログラマ:ボスプログラム、プレイヤー行動プログラム
  • プログラマ:地形アセット購入/組み込み・サウンド購入/組み込み、パーセプションニューロン触る(使えそうなら使う)
  • テクニカルアーティスト:モデルやらモーションやらエフェクトやら

でした。まぁマルチプレイといってもUnity側のプログラムがある程度できないとやることもないんで、まずは自分で持ってきたViveを自前PCのSurface Bookに繋ごうとしたらSurface Bookの出力をViveがうまく認識してくれなくて最終的に諦め(Forumとか見た限りだとノートPCの出力端子とのトラブル事案は結構多い模様なのでshoganaiね)。ということでViveのプログラミングは他の人に完全に任せることにして、ボスのプログラムを作っていこうかなー、ということに。ボス戦は、崖のような場所にボスが立っている(下半身は見せない)というイメージが共有されたので

迫り来るシリンダー撃退ゲーとして作成。雑に作ったこのシーンは、初日ずっとフル活用されることになったのだった。動画系は常時GifCamで撮ってSlackにあげてました。イメージが瞬時に共有されますし、良い内容だとテンションも上がりますし。

この時点でLeanTweenを追加。マルチプレイできるようにするので、非確定要素をいれないように、というのとそんな凝ることもないしなので弾は全部トゥイーンで制御しようかと。トゥイーンライブラリは色々ありますが、今のとこ私が選ぶならDOtweenかLeanTweenかなぁ。普通だったらDOtween選びます。ただ、今回はLeanTweenにしました、ちょっと慣れておこうかな、と思って。LeanTweenは複数のTweeenの制御とかの補助が入ってないんですが、その辺はUniRxで制御させたので全く問題なし。基本的に完了などのイベントをObservable化すれば既存トゥイーンライブラリとUniRxの統合は容易ですし、かなりいい具合にコントロールできます。実際今回は色々な挙動をそれで組みました。ところで、今ひっそりとUniRx前提のハイパフォーマンスでリアクティブなトゥイーンライブラリを作っているので、それが完成したら基本的にそれしか使いません:) まぁ、というのもあって色々なトゥイーンライブラリを試しているというのもあります。

このシーンをプレイヤー行動プログラム側に渡して、VIVEで弾き返したり防いだりを作ってもらう感じに。パーセプションニューロン触るマンはパーセプションニューロンを触りつつサウンド探しを、テクニカルアーティストはボスのモデル(これ自体は買ったもの)のUnityへのインポートとテクスチャ調整とモーション付けを、私は弾のバリエーションを作ってました。

豪速球を投げ込むバッティングセンター的イメージ。手前で弾が伸びてくるので振りにくい。二者択一(手前のキューブはプレイヤーAとプレイヤーBです、マルチ協力プレイだから!)でどっちに来るか分からないので、なんとなく緊迫感あってゲーム的でもあるよね、ということで最終的なボス行動にも採用。

ぶわーっと全方位に出すのが欲しい(VIVEのデモゲームのThe LabにあるXortexという360度シューティングのボス弾のような)というオーダーを受けて。中々いい感じに派手なので、これも採用。

あとはボツ案的な弾を作ったり、その他、この時点ではまだ夢膨らみんぐで、ボスの行動も腕をばちーんと振り下ろしてきてそれを斬撃の連打で防いで弾き返す(協力プレイなので、片方が防いでたらそれに加勢しないとダメ、とか)、などを想定したコードを準備したりAIは少しリッチにしようかな、とBehaviorTreeのライブラリ書いてたり(ノードエディタなしの基本的なランタイムだけ)、プレイヤープログラム作るチームは盾で防御する処理(最終的に削られたけれど最初は刀と盾の装備のつもりだった)をやってたら、夜0時。うーむ、時間が過ぎるのは早い。

この辺でさすがに未だにシリンダーとキューブが相手で完成形が全く見えてないのはヤヴァいでしょうということで、シーン統合しましょう祭り。特にテストで大量のアセットを抱えていたマップ作るマンがGitHubに中々Pushできないなどなどトラぶりつつも、2時ぐらいにようやく一段落。マップにモーション付きボスモデル配置して、とりあえず弾を出るようにして、でこんな具合に。

ezgif com-resize

色々アレですが、しかし中々格好よくてテンションあがりますね!その他プレイヤーのほうも入れこんだりなんなりで床に転がって仮眠とって、朝。キューブにも岩を当てはめてついに出来上がったのは……!

image

んー、悪くない。悪くないんだけど、和ではない。雲南省(適当なイメージ)とかそういう中国の高山っぽい気すらする。ここで実際完成させる仕様を概ね確定。

  • マップは明確に和テイストが出るものにリテイク
  • ボスは殴り攻撃などなし、弾のみ
  • 盾はなし、弾を打ち返してボスにダメージ与えて、一定回数食らわせたらクリア
  • マルチプレイは諦めないので作業は並走、ただし最終的にはシングルプレイが完全にプレイできるの優先

私は、ボスのモーションが二種あって、殴りつけてくるつもりでつけてもらったモーションはボスが弾を投げ飛ばしてくる(つもり)な雰囲気に適当に調整(タイミングは適当にdelayかけて目視で合わせただけでジャストとは程遠いんですが、まぁなんとなくそれっぽく見えなくもないのでヨシとした)したり、もう一個の大技っぽくやってくるモーションは、なんか岩を抱え込んでる感じにできそうな気がしたので、適当にそれっぽく位置合わせして破裂させてみることに。

resize

うん、それっぽい。地面にめり込んでってるのとかも、まぁ全然気にならないし。このボス行動は今回のハッカソンで私的な私が作った中では一番よくやりましたしょうでした。全部、偶然素材が揃っただけなんですがうまく噛み合ったということで。

この後は、マップを和テイストに差し替えて常時マップブラッシュアップ、ゲームの要素が確定したので、各種のヒットエフェクトを作ってもらって当て込みや効果音、プレイ感向上のための弾の動きなどの調整、そして諦めてないマルチプレイなどなどを時間ギリギリまで使ってなんとか完成……!(実際、最後の30分でボスのダメージエフェクトがつき、最後の15分前でボスが死ぬようになった程度にギリギリだった)

マルチプレイに関しては、Viveのセンサーが干渉してうまく二台プレイの調整ができなかったのと、もう一台のデスクトップPCを会場の無線LANに繋げなくて、というネットワーク的な問題で断念。いちおう、プログラム側はマルチ想定で動作するように最後まで組んでました、いやほんと。サーバー側、AzureのVMも一時的なものなのでということで、かなりマシンパワーの強いものに変更したりしたんですけどね、というわけでここをお見せできなかったのは残念。なので、最後の5分でマルチプレイ用のログイン待機処理を消して、リリースビルド完成。お疲れ様ー。

完成形

出来上がったものは、マップリテイクによって城が追加されたことにより「城下まで迫ってきた赤鬼を、手に持つサムライブレードにより撃退し、城を守る」という設定に。なっていた。完全後付けで。ゲーム名は特に何も考えもなく直前で「Clash of Oni Online」に大決定。

ハッカソンでの評価は特に会場でのプレゼンはなく、体験してもらって審査員が表を付けてく形式とのことで、あとはデモマンがいい感じに来場者に説明してるのを横目に私は会場を見学する:)最後の一秒までドタバタと調整を続けていた割には、目立ったバグもなくスムースにプレイできてて良かった良かった。

最終的には当日の審査ではなく後日の審査ということなので、プレゼン資料を作ったり動画を撮ったりして

Clash of Oni Online - VR Multiplay Sword Action from Yoshifumi Kawai

結果待ち……!そして発表……!受賞……!やったね!

反省点とか

当初の想定よりもViveのルームスケールを活かしてない、直立不動のスタイルになったのは、ちょっと想定外。弾を避けたりとか、近寄ったりとかもうちょっとだけアクティブなのをイメージしていたので、しょうがないといえばしょうがないのだけれど、次に何か作るのだったら動くタイプのを作りたいですねぇ。

効果音が足りなかったり、割れてたり。効果音足りないのは、岩の音を、ボス撃破音とか足すべき箇所はいっぱいですよねー。マップリテイクで時間が取れなかったのがその辺の敗因か。Viveコントローラーと刀の位置が微妙にあってなかったり、足が地面に設置してなかったりといった、プレイヤーに対する調整も甘め。shoganai。この辺はViveプログラミングにて慣れてれば、スッと合わせられる話だと思うので、経験値を積もう。ボスの全方位弾が実は全方位じゃなくて左に寄ってるのは普通にロジックのバグ……。リテイク後のマップのクオリティが急ぎで用意しただけあってリテイク前に比べると低い(雑に光源足すためだけの灯籠を並べるとかしたかった)、ボスを遠方に置く形になってしまったのでスケール感が出なかった、など。

とか、まぁアラはいくらでも見つかりますが、基本的にはよくやったと思ってる……!よ!チームメンバーが全員、より良くするために自分の仕事を探して作りきっていったというのは純粋なハッカソンの楽しさという感じで、疲労困憊だけど達成感はありますね。

それとViveでのプログラミングは、結構ゲーム作成入門(Unity入門)にいいかもですね。3Dのプレイヤーの操作ってモーションつけたり色々ハードル高いですが、Viveならすぐに手の動きがキャプチャされて自由に動かせるアクションが作れるので、よくあるシューティングとかブロック崩しとか作っていくよりも楽しいんじゃないかな。(今のVR経験値が少ない現状なら)VRで空間を見て、Viveコントローラーで自由に操作できるというのは、それだけで楽しい体験を作れちゃいますしね。

そんなわけで、家でもViveを設置したしGeforce GTX 1080搭載PCも買ったので、ちょいちょいとVive用に何か作っていきたいという気持ちを強くしました。ので、ちょいちょいと出していければいいですねー。

ObserveEveryValueChanged - 全てをRx化する拡張メソッド

ブードゥーの秘術により、INotifyPropertyChanged不要で、値の変更を検知し、IObservable化します。例えばINotifyPropertyChangedじゃないところから、WidthとHeightを引き出してみます。

using Reactive.Bindings.Extensions;
 
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
 
        this.ObserveEveryValueChanged(x => x.Width).Subscribe(x => WidthText.Text = x.ToString());
        this.ObserveEveryValueChanged(x => x.Height).Subscribe(x => HeightText.Text = x.ToString());
    }
}

wpfgif

なるほど的確に追随している。ソースコードはGitHub上に公開しました。

ReactivePropertyと組み合わせることで、そのままバインダブルに変換することも可能です。

public class MyClass
{
    public int MyProperty { get; set; }
}
 
public partial class MainWindow : Window
{
    MyClass model;
    public IReadOnlyReactiveProperty<int> MyClassMyProperty { get; }
 
    public MainWindow()
    {
        InitializeComponent();
 
        model = new MyClass();
        this.MyClassMyProperty = model.ObserveEveryValueChanged(x => x.MyProperty).ToReadOnlyReactiveProperty();
    }
}

ついでにokazukiさんが、ReactiveProperty v2.7.3に組み込んでくれましたので(今のところ).NET版では是非是非に使えます。UWP用とかXamarin用とかもきっとやってくれるでしょう(他人任せ)

仕組み

CompositionTarget.Renderingに引っ掛けて、つまり毎フレーム監視を走らせています。もともとUniRxのために作った機構を、そのままWPFに持ってきました。CompositionTarget.Renderingは、アニメーション描画などでも叩かれている比較的低下層のイベントで、これより遅いと遅れを人間が検知できちゃうので影響が出るし、これより早くても視認できないので意味がない。という、ぐらいの層です。こういった用途ではベストなところ。

毎フレーム監視がありかなしか。ゲームエンジンだと、そもそもほとんどが毎フレームごとの処理になっているので違和感も罪悪感もないのですけれど、全てがイベントドリブンで構築されている世界にそれはどうなのか。もちろん、原則はNoです。素直にINotifyPropertyChangedを書くべきだし、素直にReactivePropertyを書くべきでしょう。

ただ、アニメーションでも使われるしデバイスのインプット(LeapMotionとか)もその辺に引っ掛けるようなので、ここにちょっとプロパティに変更があるかないかのチェック入れるぐらい別にいいじゃん(どうせCPU有り余ってるんだし)、みたいな開き直りはあります。かなり。割と。

ObserveEveryValueChangedは、毎フレーム回っているような低下層の世界から、イベントドリブン(リアクティブ)な世界に引き上げるためのブリッジとしての役割があります。そう思うと不思議と、よく見えてきませんか?ただ「毎フレームポーリングかよ、ぷぷw」とかって一笑するだけだと視野が狭く、もう少しだけ一歩踏み込んで考えてみると思考実験的に面白い。私はコード片に意思を詰め込んでいくのが好きですね。哲学といってもいいし、ポエムでもある。そこには幾重も意味が込められています。

PhotonWire - Photon Server + Unityによる型付き非同期RPCフレームワーク

というのを作りました。Unityでネットワークマルチプレイヤーゲーム作るためのフレームワーク。といっても、100%自作じゃなくて、基本的にPhoton Serverというミドルウェアの上に乗っかるちょっと高級なレイヤーぐらいの位置づけです。去年の9月ぐらいに作った/作ってるよ、というのは発表していたのですが、それからかれこれ半年以上もpublicにしてないベイパーウェアだったのですが(グラニ社内では使ってました)、重たい腰を上げてやっと、やっと……、公開。

謳い文句は型付き非同期RPCフレームワークで、サーバー側はC#でasync/awaitを使ったメソッド実装、Unity側はそこから生成されたUniRx利用のメソッドを呼ぶだけで相互に通信できます。それでなにができるかというと、Visual StudioでUnity -> Server -> Unityと完全に一体化してデバッグできます。全部C#で。もうこれだけで最強でしょう。他は比較にならない。勝った。終わった。以上第一部完。

真面目に特徴、強みを上げると、以下のような感じです。

  • 完全タイプセーフ。サーバー-サーバー間は動的プロキシ、クライアント-サーバー間は事前クライアント生成。
  • IDLレス。C#サーバーコードのバイナリを元にして、クライアントコードを生成するので、普通にサーバーコードを書くだけ。面倒なIDLはゴミ箱ぽい。
  • 高性能。サーバーはasync/awaitで、クライアントはUniRxにより完全非同期で駆動。特にサーバーのC#コードはIL直書きも厭わずギチギチに最適化済み。
  • 事前生成シリアライザによるMsgPackでのシリアライズ/デシリアライズ。デシリアライズは更にマルチスレッド上で処理してUniRxでメインスレッドにディスパッチするのでフレームレートに一切影響を与えない。
  • Visual Studioとの完全な統合。高いデバッガビリティと、Analyzer利用を前提にしたフレームワーク構成はVS2015時代の新しい地平線。
  • 外部ツール「PhotonWire.HubInvoker」により外からAPIを直接叩ける。

HubInvokerは私にしては珍しく、ちゃんと見た目にこだわりました。これの外観の作り方はMaterial Design In XAML Toolkitでお手軽にWPFアプリを美しくで記事にしてます。

Photon Serverを選ぶ理由

Unityでもネットワーク系は色々な選択肢があると思います。

  • UNET
  • PUN + Photon Cloud
  • Photon Server(SDK直叩き)
  • モノビットエンジン
  • WebSocketで自作
  • MQTTで自作

このあたりは見たことある気がします。そのうちUNETは標準大正義だしAPIもProfilerも充実してる感なのですが、uNet Weaver Errorがムカつくので(コンパイルができなくなるという絶望!特にUniRx使ってると遭遇率が飛躍的に上昇!)、それが直らないかぎりは一ミリも使う気になれない。というのと、サーバーロジックを入れ込みたいどうしてもとにかくむしろそれがマスト、な状況の時にというか割とすぐにそうなると思ってるんですが、Unity純正だと、逆にUnityから出れないのが辛いかな、というのはありますね(ロードマップ的にはその辺もやるとかやらないとかあった気がしますが、まぁ遠い未来ということで)。Unity外で弄れるというのは、サーバーロジックだけじゃなくHubInvokerのようなツールを作れるっていうのも良いところですね。大事。なので、標準大正義は正しくも選べないのです。

モノビットはよく知らないので。C++でサーバーロジックは書きたくないなあ、今はC#も行けるんですっけ?

自作系は、あんまりそのレイヤーの面倒は見たくないので極力避けたい。別に動くの作るのはすぐでも、まともにちゃんと動き続けるの作るのは大変なのは分かりきってる話で。トラブルシュートも泣いちゃう。そこに骨を埋める気はない。あと、自作にするにしてもプロトコルの根底の部分で安定してるライブラリがあるかないかも大事で(そこまで自作は本当に嫌!)、Unityだとただでさえそんなに選択肢のないものが更に狭まるので、結構厳しい気がするのよね。実際。

Photonといって、Photon Cloudの話をしているのかPUN(Photon Unity Network)の話をしているのか、Photon Serverの話をしているのか。どれも違く、はないけれど性質は違うのだから一緒くたに言われてもよくわからない。さて、PUN。PhotonのUnityクライアントは生SDKが低レイヤ、その上に構築されたPUNが高レイヤのような位置づけっぽい感じですが、PUNは個人的にはないですね。秒速でないと思った。PUNの問題点は、標準のUnity Networkに似せたAPIが恐ろしく使いづらいこと。標準のUnity Network自体が別に良いものでもなんでもないレガシー(ついでにUnity自体も新APIであるUNETに移行する)なので、それに似てて嬉しい事なんて、実際のとこ全くないじゃん!もうこの時点でやる気はないんですが、更にPhoton Serverで独自ロジック書いたらそこははみ出すので生SDK触るしかないのだ、なんだ、じゃあいらないじゃん?Client-Client RPCも別になくてもいいし、というかなくていいし。

Photon Server。C++のコアエンジンってのは言ってみればASP.NETにおけるIISみたいなもので、開発者は触るところじゃない、直接触るのはサーバーSDKとクライアントSDKだけで、つまり両方ピュアC#。その上では普通にC#でガリガリと書ける。いいじゃん。両方ピュアC#というのが最高に良い。サーバーはWindowsでホストされる。それも最高に良い。プロトコルとかはゲーム専用で割り切ってる分だけ軽量っぽい。うん、悪くないんじゃないか。

また、ホスティングは結構優秀です。まず、無停止デプロイができる(設定でShadowCopy周りを弄ればOK)。これ、すっごく嬉しい。この手のは常時接続なのでデプロイ時に切断するわけにもいかないし、これ出来ないとデプロイの難易度が跳ね上がっちゃいますからねぇ。また、1サーバーで擬似的に複数台のシミュレートなどが可能です。実際、グラニでは6台構成クラスタのシミュレートという形で常に動かしていて、どうしても分散系のバグを未然に防ぐには重要で、それがサクッと作れるのは嬉しい。脚周りに関しては、かなり優秀と思って良いのではないでしょうか。

PhotonWireの必要な理由

Photon Serverがまぁ悪くないとして、なんでその上のレイヤーが必要なのか。これは生SDKを使ったコードを見てもらえれば分かるかしらん。

// 1. クライアント送信
var peer = new CliendSidePeer(new MyListener());
peer.OpCustom(opCode:10, parameter:new Dictionary<byte, object>());
// 2. サーバー受信
protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
{
    switch (operationRequest.OperationCode)
    {
        case 10:
           // Dictionaryに詰まってる
            var parameter = operationRequest.Parameters;
            HogeMoge(); // なんか処理する
            // 3. 送り返す
            this.SendOperationResponse(new OperationResponse(opCode:5), sendParameters); // 
            break;
        // 以下ケース繰り返し
        default:
            break;
    }
}
public class MyListener : IPhotonPeerListener
{
    // 4. クライアント受信
    public void OnOperationResponse(OperationResponse operationResponse)
    {
        // 返ってきたレスポンス
        switch (operationResponse.OperationCode)
        {
            case 5:
                // なんかする
                break;
        }
    }
}

問題点は明白です。原始的すぎる。byteパラメータ、Dictionaryを送って受け取りそれをswitch、送り返してやっぱswitch。こうなると当然長大なswitchが出来上がってカオスへ。また、クライアント送信とクライアント受信がバラバラ。コネクションで送信した結果を受け取るのが、独立した別のListenerで受け取ることになる、となると、送信時にフラグONで受信側でフラグチェック祭り、Listener側のフラグ制御が大変。送信したメッセージと戻り受信メッセージだという判別する手段がないので、並列リクエストが発生するとバグってしまう。

これをPhotonWireはHubという仕掛け(とUniRx)で解決します。

image

ようするにMVCのControllerみたいな感じで実装できます、ということですね。また、逆に言えば、PhotonWireはそんなに大きな機能を提供しません。あくまで、このswitchやちょっとしたシリアライゼーションを自動化してあげるという、それだけの薄いレイヤーになっています。なので、PhotonWireによるコードが素のPhoton Serverによるものと少し異なるからといって、あまり警戒する必要はありません。実際、薄く作ることは物凄く意識しています。厚いフレームワークは物事の解決と同時に、別のトラブルを呼び込むものですから……。

ちなみにPhotonWireを通すことによる通信のオーバーヘッドは4バイトぐらいです。それだけで圧倒的に使いやすさが向上するので、この4バイトは全然あり、でしょう。

Hub

HubというのはASP.NET SignalRから取っています。というか、PhotonWireのAPIはSignalRからの影響がかなり濃いので、ドキュメントはSignalRのものを漁れば20%ぐらいは合ってます(全然合ってない)

// Unityクライアント側で受け取るメソッド名はインターフェイスで定義
public interface ITutorialClient
{
    [Operation(0)]
    void GroupBroadcastMessage(string message);
}
 
[Hub(100)]
public class Tutorial : PhotonWire.Server.Hub<ITutorialClient>
{
    // 足し算するだけのもの。
    [Operation(0)]
    public int Sum(int x, int y)
    {
        return x + y;
    }
 
    // 非同期も行けます、例えばHTTPアクセスして何か取ってくるとか。
    [Operation(1)]
    public async Task<string> GetHtml(string url)
    {
        var httpClient = new HttpClient();
        var result = await httpClient.GetStringAsync(url);
 
        // PhotonのStringはサイズ制限があるので注意(デカいの送るとクライアント側で落ちて原因追求が困難)
        // クラスでラップしたのを送るとPhotonの生シリアライズじゃなくてMsgPackを通るようになるので、サイズ制限を超えることは可能 
        var cut = result.Substring(0, Math.Min(result.Length, short.MaxValue - 5000));
 
        return cut;
    }
 
    [Operation(2)]
    public void BroadcastAll(string message)
    {
        // リクエスト-レスポンスじゃなく全部の接続に対してメッセージを投げる
        this.Clients.All.GroupBroadcastMessage(message);
    }
 
    [Operation(3)]
    public void RegisterGroup(string groupName)
    {
        // Groupで接続の文字列識別子でのグループ化
        this.Context.Peer.AddGroup(groupName);
    }
 
    [Operation(4)]
    public void BroadcastTo(string groupName, string message)
    {
        // 対象グループにのみメッセージを投げる
        this.Clients.Group(groupName).GroupBroadcastMessage(message);
    }
}

async/awaitに全面対応しているので、同期通信APIを混ぜてしまっていて接続が詰まって死亡、みたいなケースをしっかり回避できます。属性をペタペタ張らないといけないルールは、Visual Studio 2015で書くとAnalyzerがエラーにしてくるので、それに従うだけで良いので、かなり楽です。

プリミティブな型だけじゃなくて複雑な型を受け渡ししたい場合は、DLLを共有します。

// こんなクラスをShareプロジェクトに定義して、Server側ではプロジェクト参照、Unity側へはビルド済みDLLをコピーする
public class Person
{
    public int Age { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
// サーバーがこんなふうに戻り値を返して
[Operation(1)]
public Person CreatePerson(int seed)
{
    var rand = new Random(seed);
 
    return new Person
    {
        FirstName = "Yoshifumi",
        LastName = "Kawai",
        Age = rand.Next(0, 100)
    };
}
// Unity側では普通に受け取れる
proxy.Invoke.CreatePersonAsync(Random.Range(0, 100))
    .Subscribe(x =>
    {
        UnityEngine.Debug.Log(x.FirstName + " " + x.LastName + " Age:" + x.Age);
    });

プロジェクトの構成はこんな感じ。シームレス。

image

また、オマケ的に、Unity側でのエディタウィンドウではコネクションの接続状況と送受信グラフがついてきます。UNETの立派なProfilerに比べるとショボすぎて話にならないんですが、ないよりはマシかな、と。

サーバー間通信

Photon Serverはサーバーとサーバーを接続してクラスタを作れるのですが、その通信もHubを使ったRPCで処理しています。

// ServerHub(呼ばれる方)
[Hub(54)]
public class MasterTutorial : PhotonWire.Server.ServerToServer.ServerHub
{
    [Operation(0)]
    public virtual async Task<int> Multiply(int x, int y)
    {
        return x * y;
    }
}
 
// ClientHub(呼ぶ方)
[Hub(99)]
public class Tutorial : Hub
{
    [Operation(0)]
    public async Task<int> ServerToServer(int x, int y)
    {
        var val = await GetServerHubProxy<MasterTutorial>().Single.Multiply(x, y);
        return val;
    }
}

この見た目、直接呼んでるかのように書けるサーバー間通信は、実行時には以下のように置き換わってネットワーク呼び出しに変換されています。

image

なので、ServerHubはかならず戻り値はTaskじゃないとダメです(Analyzerが警告してくれます)。昔はこの手の処理を、メソッド呼び出しのように隠蔽する場合って、同期的になっちゃって、でもネットワーク呼び出しなので時間かかってボトルネックに、みたいなパターンが多かったようですが、今はTask[T]があるので自然に表現できます。このへんも含めてTask[T]が標準であることの意味、async/awaitによる言語サポートは非常に大きい。

この辺りの詳しい話は以下のスライドに書いています。

Metaprogramming Universe in C# - 実例に見るILからRoslynまでの活用例 from Yoshifumi Kawai

ネットワーク構成

PhotonWireは特に何の既定もしません。Photonが自由に組める通り、どんな組み方もできるし、どんな組み方をしてもPhotonWireでの呼び出しに支障は出ません。

のはいいんですが、その時、ClientPeer, InboundS2SPeer, OutboundS2SPeerの3種類のPeerを持つように、PhotonWireもまたHub, ServerHub, ReceiveServerHubとそれぞれに対応する3種のHubを持っています。3つ、これは複雑で面倒。

しかしPhotonWireはネットワークの複雑さの隠蔽はしません。やろうと思えばできますが、やりません。というのも、これ、やりだすと泥沼だから。賢くやりたきゃあAkkaでもなんでも使ってみればよくて、自分で書いたら一生終わらない。Photonのネットワークは本当に全然賢くなくて、ただたんに直結で繋いでるという、それだけです。そんなんでいい、とまではいいませんが、そうなら、それに関しては受け入れるべきでしょうね。勘違いしちゃあいけなくて、フレームワークは複雑さを隠蔽するもの、ではないのです。

ともあれ、最低限の賢くなさなりに、スケールしそうな感じに組み上げることは可能なので、全然良いとは思ってますよ!

できないこと

ポンと貼り付けてtransformが自動同期したり、いい感じに隙間を補完してくれたりするものはありません。ただ、Client-Server RPCがあれば、それは、その上で実装していくものだと思うので(いわゆるNantoka ToolkitとかNantoka Contribの範疇)、しゃーないけれど、自前で作ろうという話にはなってきますね。↑のネットワーク構成の話も、隠蔽とまではいかなくても、決まった構成になるのだったらそれなりにバイパスするいい感じのユーティリティは組んでいけるだろうから、その辺のちょっとした増築は、やったほうがいいでしょう。

まとめ

現状実績はないです(今、公開したばかりですからね!)。ただ、グラニで開発中の黒騎士と白の魔王というタイトルに投下しています。

半年以上は使い続けているので、それなりには叩かれて磨かれてはいるかなあ、と。大丈夫ですよ!と言い切るには弱いですが、本気ですよ!とは間違いなく言えます。DLLシェアや自動生成周りが複数人開発でのコンフリクトを起こしがちで、そこが改善しないと大変かなー、というところもありますが、全般的にはかなり良好です。

ちょっと大掛かりだったり、Windows/C#/Visual Studioベッタリな、時代に逆行するポータビリティのなさが開き直ってはいるんですが、結構使い手はあると思うので試してみてもらえると嬉しいですね!あと、大掛かりといっても、知識ゼロ状態からだったら素のPhoton Server使うよりずっと楽だと思います。そもそもにPhotonWireのGetting Startedのドキュメントのほうがよほど親切ですからねぇ、Visual Studioでのデバッグの仕方とかも懇切丁寧に書いてありますし!

VR時代のマルチプレイヤーって結局どうすんねん、と思ってたんですが、Project TangoのサンプルがPhotonだしAltspaceVRもPhotonっぽいので、暫くはPhotonでやってみようかなー。という感です。

MarkdownGenerator - C#におけるAPI Reference生成のためのドキュメントツール

APIリファレンス作りたい?Sandcastle。以上。終了。あるいはdotnet/docfxが良いのではないでしょうか。こいつはdotnet配下にあるように、MSの今後のOSS系のはこれでドキュメント生成されていく可能性があります。

というのは置いといて、私的には実のところ、あんまり重要視していませんでした、ドキュメントツール。.chmにはいい思い出がなくて、というか別に見ないじゃん?htmlで出力してもなー、なんかゴチャゴチャしてて汚いしなー。一方でJavaScriptなんかは様々な格好良くフォーマットされた形式で色々出てるのであった。いいじゃん。いいね。

さて、もう一つ。HTMLで出力しても置き場にこまる。GitHub Pagesにはそんないい思い出がない。別にあんなところをフロントにするよりもリポジトリのアドレス直のほうが断然いいじゃん、みたいな。というわけでアレだ、GitHub Wikiだ。あそこをゴミ置き場にすればいいんだ。という発想で、まずUniRxのリファレンスをGitHub Wikiに置いてみました。

UniRxは、namespaceを意図的にある程度平ったくしてるので、ちょっとごちゃってますが、まぁまぁいいんじゃない?それなりに見れる。悪くはない。少なくともないよりは100億倍良い。

私的にはIntelliSenseがドキュメントだ!みたいな意識がそれなりにあって、最初のチュートリアルみたいなドキュメントがあったら、あとはそれを手がかりにあとはIntelliSenseでなんとかしようぜ、的なところが。実際Roslynなんかはそんな感じがする。Getting Startedはそれなりに厚い、けど全貌からは程遠い。でもAPIドキュメントはない。さあ、IntelliSenseで宝探しだ。って。肯定もしないけれど否定もしない、そういうのも今風よね。でも、まあこの程度のAPIリファレンスでも生成してやると、それはそれで良いな、って思ったのだ。です。

MarkdownGenerator

生成は自家製こんそーるあぷりで行ってます。というわけで公開しました。

dllとxmlを渡すとmdと目次用のHome.mdをばらまくので、GitHub Wikiに投げ込みます。そう、GitHub Wikiはご存じの方も多い通り、それ自体がgitで管理されててCloneできるのです。さいこー。というわけでそのままPushするだけ。Good。完璧。これなら、CIなんかでフックして毎回生成して投げ飛ばしてあげてもいい。よね。

生成結果のStyleはちょっとまだまだ試行錯誤中。まあでも割とこんなもんでいいんちゃうんちゃうん?ユースケースの9割ぐらいはカバーできているでしょう。それ以上はノイズということで。

その他ツール

Sandcastleはそもそも出力をカスタマイズできるので、もう少し真面目というかガッチリしたものが必要ならば、maxtoroq/sandcastle-mdあたりを使ってMarkdownを出力してやると良いでしょう。これなら、きっちりとSandcastleで出力される情報が全部そのまま入ってるので、ちゃんとしてる感は圧倒的に高いです。また、繰り返しますけれどDocFXは今からやるなら最有力候補な気がします。DLLからじゃなくてRoslynでプロジェクトファイルから解析したりとか今風。あくまでstatic file generatorなのでmdじゃなくてhtml出力なので、Wikiに投げ飛ばす用途には向かないのと、ちょっと複雑、使いこなすのは難しい、かな、まあ相応には良さそうかとは。

MarkdownGeneratorは、ちゃんとしてないなりに、私的に重要視してる情報がパッと一覧で見やすくする、ということを重視しているので……。あと、なんかSandcastle使いたくないんだよねー、心理的に。なんだろうね、レガシー臭するからなのかな。食わず嫌いなだけって話でもあるのだけれど。

何れにせよ、GitHubであってもなくてもいいだろうけれど、API Referenceを投げ飛ばす場としては、そういうところ(どういうところ?)がいいですね。独立してるよりもリポジトリに近い場所のほうが素敵度は高い。気がする。あとはなんのかんのでGitHubに慣れきってるというのもあって、GitHubにあると情報がスムースに受け取れる気がするんだよね。これもなんでだろうね。でもそういうのってあるよね。

Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映

UniRx 5.3.0をリリースしました!今回のアップデートは、内部的な最適化、です。最適化は、もうそろそろあんまやるところ残ってないよね、なんて思ってたんですが、じっくり考えるとそんなことなく割とあったので埋めました。それが表題のコルーチンの省メモリと高速化です。使い方次第ではありますが、場合によっては今回のアップデートでものすごく恩恵に授かる人もいればそこそこの人もいるかもです。ともあれ基本的に内部的に変更してるだけなので、入れるだけでそれなりに高速化したりする可能性がそれなりにあります。

前回が2月だったので3ヶ月ぶりですね。あまりオペレータ追加がないので、次はオペレータ追加に集中したい気もする。なんか優先的に欲しいのあればリクエストもどうぞ(Observable.Windowとかいい加減そろそろ入れろよって話なんですが)

MicroCoroutine

今回の大きい変化はMicroCoroutine(と、自称してる)の導入です。特に大量にEveryUpdateやEveryValueChangedを呼んでるシチュエーションにおいて10倍、というのは場合によりで大雑把なのですが、相当速くなります。

void Start()
{
    // Start 10000 Coroutines
    for (int i = 0; i < 10000; i++)
    {
        // Standard Unity Coroutine
        // StartCoroutine(Counter());
 
        // Use UniRx 5.3 - MicroCoroutine
        MainThreadDispatcher
          .StartUpdateMicroCoroutine(Counter());
    }
}
 
IEnumerator Counter()
{
    while (true)
    {
        count++;
        yield return null;
    }
}

こんな10000個、単純なコルーチンを起動するコードがあったとして

image

大きく違いがでます。ちょっと恣意的すぎではあるんですが、UniRxはコルーチンを簡単にかけるが故に、これに近いシチュエーションってのが意図せず起こりがちではありました。また、Resources.LoadAsyncなど非同期系APIからの取得に関しても、一時的に多くのコルーチンを起動するシチュエーションはあり得るのではないでしょうか。

性能改善した理由は、基本的にはUnityの公式ブログUPDATE()を10000回呼ぶで紹介されていることの話で、10000個のUpdateは遅くて、配列に詰めて直接ループで呼ぼうぜ、と。どうせUpdate内のメソッドは呼ばれてC#の領域で実行されるんだから、マネージド(C#)-アンマネージド(C++)の繋ぎのレイヤーは純粋にオーバーヘッドになってくるよ、と。なるほどそうだねそりゃそうだねぇ。それはStartCoroutineにも言えて、というかコルーチンのほうがもっと性能劣化度が大きいんですよね。

この記事は非常に素晴らしくて、大量にモノ出して速度遅くなってるのがスクリプト起因なら、マネージャー立ててまとめて、あとUpdateに限らずマネージド-アンマネージドの繋ぎをやってる部分が遅いだろうからそこを適切に取り除ける限り除けば、全然まだまだそれなりに捌ける余裕は残ってるぜ。ということで、むしろ希望に満ちていていい感じです。実際、ハイパフォーマンスを謳うDOTweeenとかのライブラリもそんな感じですね、動かすものそれぞれにUpdateするコンポーネントを挿したりはしない、中央管理で動かすのだ、と。

さて、UniRxでは幾つかのメソッドはコルーチン依存でループを回しています。Observable.EveryUpdateとかEveryValueChangedとか。少しに使う分にはいいんですが、気楽に使えるが故に、大量に使うと、10000個とまではいかなくてもやっぱり、それぞれがコルーチンを起動することによるマネージド-アンマネージドオーバーヘッドがそのまま乗っかってきてしまいます。というわけで、やはりコルーチン自前管理の道を進むしかない……。幸い、自前管理で問題になる機能面での低下に関しては、UniRx自体がコルーチンを凌ぐだけの機能を提供しているので、気にしないでよし。というわけで純粋にいかにコルーチン(IEnumerator)を高速に回転させ、高速にメンテナンスするかにだけ集中すればよし。

回転させるのはforループ回すだけの話なんですが、マネージャー作ろうぜ、となった時に、Listに詰めるのはいいんですが、面倒くさいのは削除。削除は要注意で、単純にListのRemoveやって済ませたりするのは結構アレです(Removeは相当高コストな操作です)。かといってDictionaryやSet、LinkedListでやるなんていうのは論外で(列挙の性能が死ぬので本末転倒)、基本的に配列で頑張るべきなんですが、さてはて。結局、その辺のめんどーを見るのがめんどーだからUpdateやStartCoroutineでぶん回すのだ。割と本気で。

ではどうしたか、というと、UniRxのMicroCoroutineのアプローチはRemoveしない。です。しない。空いた部分はnullで埋めて純粋にスキップするだけにする。多少の空きなら、いちいち削るよりもスキップさせたほうが速い。しかし、それだけだとブヨブヨと膨らみ続けてしまうので、xフレーム毎に空きスペースに詰めなおして小さくします。縮める際も前の方に整列させるんじゃなくて、空きスペースに対して後ろから埋めるようにするので、順番はグチャグチャになります。その代わり余計な配列へのセットが発生しないので速い。そして膨らんだ配列は放置して膨らんだままにします、終端のインデックスだけ記録して管理するのみ(ところでアセットストアにアップデート申請出してから気づいたのですが、この配列の使い方なら定期的なお掃除じゃなくて、動かしながら埋めるようなコードにするのも可能っぽい感、なので次回アップデートでそうします)

というわけで、UniRxのMicroCoroutineは中央集権的なので多少膨らむことが許される(でしょう!)ことを利用して、とにかく高速にコルーチンを捌く、ということだけに集中してます。ので速い。下手に自前管理するよりも速いかもしれませんし、Updateで監視するよりもObserveEveryValueChangedのほうがむしろ速い、Rxで書いたほうが速い、みたいな逆転現象も全然発生しうるような話になります。

ObserveEveryValueChanged
EveryUpdate 
EveryFixedUpdate
EveryEndOfFrame
NextFrame
TimerFrame 
IntervalFrame
DelayFrame 
SampleFrame
ThrottleFrame
ThrottleFirstFrame
TimeoutFrame

この辺りのメソッドを使った場合、内部の実装がMicroCoroutineに差し替わったので自動的に恩恵に預かれます。コルーチン -> Observable変換に関しては FromMicroCoroutine が追加されました。基本的にはFromCoroutineと一緒なのですが、MicroCoroutineではyield returnするのはnullだけにしてください、それ以外には対応してません(UnityEditor上ではWarning出して警告します)。MicroCoroutineの制約はそれなんですが、なんだかんだで、8割ぐらいはyield return nullだけで成立するんちゃうんちゃうん、みたいな。賢くやろうとすればもう少しは出来なくもないんですが、シンプルで高速なコルーチンの回転を損ねちゃうのでナシ。IEnuemrator.Currentの呼び出しや、その型チェックすら省きたい。残り2割ぐらいなら普通にStartCoroutineすればいいじゃん、ということで。実際、UniRxの↑のメソッドはそれでかなり置き換えることが出来る、ということを発見できたので、全面的に導入する気になったのです。

また、最悪待ちたい場合は、isDoneのループを回すようにToYieldInstruction経由でIObservableを待てるので、大抵のことはなんでもできます。

IEnumerator MicroCoroutineWithToYieldInstruction()
{
    var www = ObservableWWW.Get("http://aaa").ToYieldInstruction();
    while (!(www.HasResult || www.IsCanceled || www.HasError)) // 3つもプロパティ並べるのダルいので次回アップデートでIsDoneを追加します予定
    {
        yield return null;
    }
 
    if (www.HasResult)
    {
        UnityEngine.Debug.Log(www.Result);
    }
}

もっとプリミティブに直接利用したい場合は、StartCoroutineの代わりにMainThreadDispatcherに3つ生やしてあります。

MainThreadDispatcher.StartUpdateMicroCoroutine
MainThreadDispatcher.StartFixedUpdateMicroCoroutine
MainThreadDispatcher.StartEndOfFrameMicroCoroutine

それぞれがコルーチンを消費するタイミングで、まぁ普通はStartUpdateMicroCoroutineを使えばよいでしょふ。もし大量のStartCoroutineがプログラム中にあるのなら、これに差し替えるだけで本当にすっごく速くなるでしょう。ほんと。

SubscribeWithState

ここから先はUniRxのアップデートの話だけ。そして本当にMicro Micro Microな最適化であんま意味はないんですが、まず、SubcribeWithStateを追加しました。これによって何が変わるか、というと、例えば……

// Before
public static IDisposable SubscribeToText(this IObservable<string> source, Text text)
{
    return source.Subscribe(x => text.text = x);
}
 
// After
public static IDisposable SubscribeToText(this IObservable<string> source, Text text)
{
    return source.SubscribeWithState(text, (x, t) => t.text = x);
}

という感じの使い方ができます。どういう違いが出るのかというと、以前にUnityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方という記事の中で説明したのですが、ラムダ式はその中身によってコンパイル時に生成されるコードがかなり変わってきます。で、最速なのはそのメソッド内だけで完結していて外部の変数等には一切触っていない状態。onNextはActionなので、副作用かける際にどうしても外部変数をキャプチャしてしまうことが多いんですよね。そこでSubscribeWithStateを使うと、必要な変数を閉じ込めることができるので最速ゴミなしの形で記述できます。

ただまぁ、これやると、じゃあSelectやWhereなんかもState取れたほうがいいんですか?(理屈上はそうです)、とか、ああクロージャ殺さなきゃ死ね死ね死ね、とか思ったりしそうなのですけれど、Subscribeの回数ってパイプライン内の実行頻度に比べれば圧倒的に少なくなるはずなんですよね。だから全体のバランスで見たら無視できるといっても過言ではないはず、特にクロージャでちょっとゴミが出る程度の話は。

なのであんま神経質にやることはないんですが、↑のSubscribeToTextのようなそんな手間もかからないし、UIとかシーンの初期化時にいっぱい登録される可能性があるようなものでライブラリ的な部分でカバーできる質のものならば、少しだけ気を使ってあげると気は安らぐかもしれません。

ReactiveCommand

ReactiveCommandは.NET版のReactiveProeprtyにあった、最後のパーツなんですが、どうなんでしょうね、本来はViewModelのレイヤーのためなんですが、UnityだとPresenterにUI要素がセリ出してきてるのでイマイチベンリかどうか分からなくて入れてなかったんですが。一応、こんな風に使えます。

public class Player
{
   public ReactiveProperty<int> Hp;
   public ReactiveCommand Resurrect;
 
   public Player()
   {
        Hp = new ReactiveProperty<int>(1000);
 
        // If dead, can not execute.
        Resurrect = Hp.Select(x => x <= 0).ToReactiveCommand();
        // Execute when clicked
        Resurrect.Subscribe(_ =>
        {
             Hp.Value = 1000;
        }); 
    }
}
 
public class Presenter
{
    public Button resurrectButton;
 
    Player player;
 
    void Start()
    {
      player = new Player();
 
      // If Hp <= 0, can't press button.
      player.Resurrect.BindTo(resurrectButton);
    }
}

buttonのinteractableとonClickが抽象化されたもの、って感じですね。

その他

リリースノートから。

Add : ReactiveCommand
Add : MainThreadDispatcher.StartUpdateMicroCoroutine, StartFixedUpdateMicroCoroutine, StartEndOfFrameMicroCoroutine
Add : Scheduler.MainThreadFixedUpdate, MainThreadEndOfFrame
Add : ToYieldInstruction(cancellationToken)
Add : Observer.Create(onNext/onNext, onError/onNext, onCompleted) overload
Add : IReadOnlyReactiveProperty.SkipLatestValueOnSubscribe
Add : Observable.WhenAll overload (IObservable<Unit>(params IObservable<Unit>[] sources), this becomes breaking changes)
Add : Observable.FromMicroCoroutine
Add : Observable.AsSingleUnitObservable
Add : Observable.SubscribeWithState
Add : Observable.CreateWithState
Add : Disposable.CreateWithState
Improvement : Use MicroCoroutine on `ObserveEveryValueChanged`, `EveryUpdate`, `EveryFixedUpdate`, `EveryEndOfFrame`, `NextFrame`, `TimerFrame`, `IntervalFrame`, `DelayFrame`, `SampleFrame`, `ThrottleFrame`, `ThrottleFirstFrame`, `TimeoutFrame`
Improvement : Performance improvement for Observable.Range, Repeat when scheduler is Scheduler.Immediate
Improvement : Use Time.unscaledDeltaTime in IgnoreTimeScaleMainThreadScheduler
Fix : ReadOnlyReactiveProperty(source, initialValue) does not publish initial value on subscribe
Fix : IReadOnlyCollection has set indexer
Fix : Ambigious property of IReactiveCollection.Count, Indexer
Fix : Throw invalid error when ObservableWWW.LoadFromCacheOrDownload failed.
Breaking Changes : Added IReadOnlyReactiveProperty.HasValue
Breaking Changes : AsyncConvertsion scheduler to Scheduler.MainThread on WebGL build(WebGL doesn't support ThreadPool)
Other : Update UniRxAnalyzer 1.4.0.1 https://www.nuget.org/packages/UniRxAnalyzer

ToYieldInstructionはUniRx 5.0 - 完全書き直しによるパフォーマンス向上とヒューマンリーダブルなスタックトレース生成で説明しているのですが、Unity 5.3以降のCustomYieldInstuctionを応用したもので、IObservableをコルーチンで処理できるようにするやつで、結構お薦め機能です。MicroCoroutineで回すための補助にもなりますし。

SchedulerにMainThreadFixedUpdateとMainThreadEndOfFrameを足しました。ObserveOnやTimerなどで、その辺の細かい制動をしたい方にどうぞ。

(ReadOnly)ReactivePropertyへのSkipLatestValueOnSubscribe拡張メソッドの追加。これは、(UniRxの)ReactivePropertyはSubscribe時に必ず値をプッシュするようになってるんですが、そういった初期値を無視したいって局面は少なからずあるんですよね。Rx.NET用のReactivePropertyでは、コンストラクタでReactiveProeprtyModeとして、None | RaiseLatestValueOnSubscribe | DistinctUntilChanged を指定できるようなデザインを選んでいるのですが(というのも、Viewにデータバインディングするため構築時の初期値はnullであることが確定している、というシチュエーションが割とあるため)、UniRxのReactivePropertyではSubscribe側が選ぶというデザインにしています。この辺はフレームワークの性質の違いに合わせてるのですが、ともあれ、初期値を無視したい場合は rxProp.SkipLatestValueOnSubscribe().Subscribe() としてもらえれば。

Observable.WhenAllを、IObservable[Unit][]が相手の場合はIObservable[Unit]を返すようにしました。これは、別にUnit[]が返されても何の意味もないからというのと、それによって余計な配列確保をしないという最適化も入れています。この方が絶対に良いんですが、しかし戻り値の型が変わってしまったので破壊的変更にはなっています。最初から気づいておけば良かったですね、すびばせん。

AsSingleUnitObservableは LastOrDefault().AsUnitObservable() みたいな変換をかけるやつで、Async的な保証をかけるのにベンリというあれそれ。

あとは、んー、使ってる人は、うちの社内以外にないのでは疑惑も感じてますが、UniRxAnalyzerを更新してます。コンストラクタにIObservableを突っ込んでいた場合に誤検出していたのを修正しています。

これ、Visual Studio 2015を使って開発している人は絶対に入れたほうがいいですよ!Subscribe忘れて発火しないのに気づかなかったー、みたいなポカミスが圧倒的に防げますので。

まとめ

性能面でより気にせずにカジュアルに色々使えるようになった、というのはいいことかなー。性能面で問題出た際に「そういう使いかた想定してないから」といった却下の仕方って、あんましたくないですからね。聞いてていいものでは全くない。デザインとしてカジュアルに使えるようになっているなら、性能もちゃんと担保していかないし、そういうのが頻発するならライブラリの設計が悪い。と、思ってるので、今回のでよりちゃんと自然に使えるようになったかな、と。ObserveEveryValueChangedは個人的には最高にクールな機能だと思ってるので、気兼ねなく使って欲しいし、やっと本当に気兼ねなく使えるようになりました。

ObservableUpdateTrigger(UpdateAsObservable), Observable.EveryUpdate, Observable.EveryGameObjectUpdate とUpdateのハンドリングも3択、性能特性も三者三様。混乱との対話!別に特に何をレコメンドすることもなく、まあ素直に書くならUpdateTriggerが素直でよく。自身のUpdateループで周りますしね。EveryUpdateはMicroCoroutineなので性能特性的には良さげ、どうせAddTo(this)するならループのライフサイクルもUpdateTriggerと別に変わりはしないし(UpdateTriggerだとDisableでUpdateが回らなくなるので、まぁその辺で挙動に違いは出る)。EveryGameObjectUpdateはMainThreadDispatcherのSubjectに積まれるもので、UpdateTriggerが使える状況なら非推奨かな、あんまりSubjectに頻繁にAdd, Removeするのは性能特性的に悪手なので。UpdateTriggerもSubjectが駆動するのですが、性質的にグローバルじゃないのでAdd, Removeは局所化されるからそこまででは、に通常はなるでしょう、的な。

そんなこんなで、少なくともRxが性能面のネックでー、と言われるのは悔しい話なので、大きいものから小さいものまで、最適化ネタは常に考えてます。利用事例としても、結構ヒットしてる某社の某ゲーム(とは)や最近でた前作に続いてヒットの予感のする某ゲーム(とは)など、かなり使いこなしてる事例もあって(個人的にはとても感動した!)、ちゃんと実用的といってもいいレベルになってると思われます。弊社の開発中タイトルである黒騎士と白の魔王でもガッツリ使っているので、ご興味ある方は中途採用は絶賛募集中です:) 当たり前ですがドッグフーディングは凄く大事で、さすがにデカいバグは出てこないにしても軽微なものはちょいちょい上がってくるので、日々、堅牢さは担保されているな、とかかんとか。あと、使いすぎてるほどに使いすぎてるので、常に性能面でネックになってはいけない、性能面でネックになってはいけない、とマントラを唱えるプレッシャーになってるのもいいことです、多分きっと。

今回のアップデートでツメが甘かった案件としてはAsyncOperation.AsObservableやObservableWWWが内部的にまだFromCoroutine利用なので、FromMicroCoroutineに可能なら差し替えようかな、と。効果のほどとしては、やっぱり場合によりけりですが、初期化とかで大量に回る時は大きく変わるかも。しれない。ともあれ次回アップデートにご期待を。ただyield return wwwやasyncOperationした場合とyield return nullでisDoneチェックする場合とで、戻ってくるタイミングが異なるので、そこのルールを統一させないとかなあ。

Prev | | Next

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for Visual Studio and Development Technologies(C#)

April 2011
|
July 2018

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc