[Unity] (ローカルデータの永続化をふまえて) ZeroFormatter vs FlatBuffers

#内容
ローカルにデータを保存するときにZeroFormatterとFlatBuffersのどちらが速いか比較検討する。
この手のテストは書き込むデータタイプによって偏りがでるので公平性は無視して
ある程度、普段使いしそうなデータを書き込む。
(I/Oの速度もあるしテストする機によってもアレだしね :innocent:)

データタイプは[前回](http://qiita.com/akerusoft/items/48ee93e33c2038316064)使ったデータタイプを使用する。


#環境
Unity 5.4.1p1
Windows10 (ビルド)
IL2CPP

実行はAndroid4.0.4の低スペック機。(前回EasySaveで比較した端末と同じ)

FlatBuffers v1.4.0 (ランタイムは2016/11/1 最新のもの)
ZeroFormatter v1.0.1


#結果
まったく同じですね。
コードはgithubにあるのでシリアライズするデータを変更したり
各自の端末でテストして比較してもらえればと思います。

11回目からは関数のコール順を変更。

|  | ZeroFormatter | FlatBuffers |
|--:|--:|--:|
| 1 | 566 | 669 |
| 2 | 530 | 449 |
| 3 | 508 | 446 |
| 4 | 501 | 446 |
| 5 | 521 | 454 |
| 6 | 494 | 456 |
| 7 | 462 | 441 |
| 8 | 523 | 450 |
| 9 | 496 | 453 |
| 10 | 491 | 453 |
| 11 | 464 | 570 |
| 12 | 474 | 545 |
| 13 | 479 | 496 |
| 14 | 482 | 493 |
| 15 | 463 | 448 |
| 16 | 482 | 475 |
| 17 | 460 | 462 |
| 18 | 471 | 453 |
| 19 | 484 | 562 |
| 20 | 464 | 505 |
| Average | 490.8 | 486.3 |
| Max | 566 | 669 |
| Min | 460 | 441 |
| 0 | 0 | 0 |
| EasySaveとの比較 |  |  |
| Average | 1.00 | 0.99 |
| Max | 1.00 | 1.18 |
| Min | 1.00 | 0.96 |


ちなみにエディタ上で実行したときにできたファイルサイズは
どちらも84[byte]でした:open_mouth:


#実験内容
前回と同じgithubのリポジトリに収録している。
https://github.com/akerusoft/UnityTestZeroFormatter

```csharp:GameController.cs
using FlatBuffers;
using UnityEngine;
using UnityEngine.UI;
using System.Diagnostics;
using System.Collections;
using System.IO;
using ZeroFormatter;

namespace Test2
{
    public class GameController : MonoBehaviour
    {
        const int COUNT = 1000;
        const string ZERO_FORMATTER_FILE_NAME = "ZeroFormatter.bin";
        const string FLAT_BUFFERS_FILE_NAME = "FlatBuffers.bin";

        const string MONSTER_NAME = "スライム";

        [SerializeField]
        Text _textFlatBuffers;

        [SerializeField]
        Text _textZeroFormatter;

        long _flatbuffresElapsedTime;
        long _zeroFormatterElapsedTime;

        IEnumerator Start()
        {
            System.GC.Collect();
            Resources.UnloadUnusedAssets();
            yield return null;

            TestFlatBuffers();

            System.GC.Collect();
            Resources.UnloadUnusedAssets();
            yield return null;

            TestZeroFormatter();

            _textFlatBuffers.text = string.Format("FlatBuffers:{0}[ms]", _flatbuffresElapsedTime);
            _textZeroFormatter.text = string.Format("ZeroFormatter:{0}[ms]", _zeroFormatterElapsedTime);
        }

        void TestFlatBuffers()
        {
            Stopwatch stopWatch = new Stopwatch();

            string name = null;
            uint hitPoint = 0;
            float hitRate = 0f;
            uint speed = 0;
            uint luck = 0;

            string path = Path.Combine(Application.persistentDataPath, FLAT_BUFFERS_FILE_NAME);

            if (File.Exists(path))
                File.Delete(path);

            stopWatch.Start();

            for (int i = 0; i < COUNT; ++i)
            {
                // 読み込み
                if (File.Exists(path))
                {
                    ByteBuffer buffer = new ByteBuffer(File.ReadAllBytes(path));

                    MyFlatBuffers.Root root = MyFlatBuffers.Root.GetRootAsRoot(buffer);

                    MyFlatBuffers.Data dataType = root.DataType;

                    switch (dataType)
                    {
                        case MyFlatBuffers.Data.MonsterDataV1:
                            MyFlatBuffers.MonsterDataV1 monsterData = root.GetData(new MyFlatBuffers.MonsterDataV1());

                            name = monsterData.Name;
                            hitPoint = monsterData.Hp;
                            hitRate = monsterData.HitRate;
                            speed = monsterData.Speed;
                            luck = monsterData.Luck;
                            break;
                    }
                }

                hitPoint++;
                hitRate += 1f;
                speed++;
                luck++;

                // 書き込み
                {
                    FlatBufferBuilder builder = new FlatBufferBuilder(1);

                    int offestData;
                    MyFlatBuffers.Data dataType;

                    Offset<MyFlatBuffers.MonsterDataV1> data = MyFlatBuffers.MonsterDataV1.CreateMonsterDataV1(builder, builder.CreateString(MONSTER_NAME), hitPoint, hitRate, speed, luck);
                    offestData = data.Value;
                    dataType = MyFlatBuffers.Data.MonsterDataV1;

                    MyFlatBuffers.Root.StartRoot(builder);
                    MyFlatBuffers.Root.AddDataType(builder, dataType);
                    MyFlatBuffers.Root.AddData(builder, offestData);
                    Offset<MyFlatBuffers.Root> endOffset = MyFlatBuffers.Root.EndRoot(builder);

                    MyFlatBuffers.Root.FinishRootBuffer(builder, endOffset);

                    //byte[] bytes = builder.SizedByteArray();
                    //File.WriteAllBytes(path, bytes);

                    // 公式の実装がこうなっていたので
                    using (MemoryStream ms = new MemoryStream(builder.DataBuffer.Data, builder.DataBuffer.Position, builder.Offset))
                    {
                        byte[] bytes = ms.ToArray();
                        File.WriteAllBytes(path, bytes);
                    }
                }

                hitPoint = 0;
            }

            stopWatch.Stop();
            _flatbuffresElapsedTime = stopWatch.ElapsedMilliseconds;
        }

        void TestZeroFormatter()
        {
            Stopwatch stopWatch = new Stopwatch();

            string name = null;
            uint hitPoint = 0;
            float hitRate = 0f;
            uint speed = 0;
            uint luck = 0;

            string path = Path.Combine(Application.persistentDataPath, ZERO_FORMATTER_FILE_NAME);

            if (File.Exists(path))
                File.Delete(path);

            stopWatch.Start();

            for (int i = 0; i < COUNT; ++i)
            {
                // 読み込み
                if (File.Exists(path))
                {
                    DataRoot dataRoot = ZeroFormatterSerializer.Deserialize<DataRoot>(File.ReadAllBytes(path));
                    MonsterDataBase dataBase = dataRoot.LoadMonsterData();

                    if (dataBase is MonsterDataV1)
                    {
                        MonsterDataV1 data = dataBase as MonsterDataV1;
                        name = data.Name;
                        hitPoint = data.HitPoint;
                        hitRate = data.HitRate;
                        speed = data.Speed;
                        luck = data.Luck;
                    }
                }

                hitPoint++;
                hitRate += 1f;
                speed++;
                luck++;

                // 書き込み
                {
                    MonsterDataV1 data = new MonsterDataV1()
                    {
                        Name = MONSTER_NAME,
                        HitPoint = hitPoint,
                        HitRate = hitRate,
                        Speed = speed,
                        Luck = luck,
                    };

                    DataRoot dataRoot = new DataRoot();
                    dataRoot.SetMonsterData(data);

                    File.WriteAllBytes(path, ZeroFormatterSerializer.Serialize(dataRoot));
                }

                hitPoint = 0;
            }

            stopWatch.Stop();
            _zeroFormatterElapsedTime = stopWatch.ElapsedMilliseconds;
        }
    }
}
```

#手順
アプリを起動して落として、また起動してって繰り返すだけ。


#感想
今回書き込んだデータは速度面が全く変わらないと言っていいレベル。

FlatBuffersは実装方法を忘れがち。
前のコードみてあーだったなーとかっていうレベル。
ZeroFormatterはインスタンスを渡すだけというお手軽設計。

どちらもC#コードを自動生成するということはかわらなくても
実装面や使いやすさからすると圧倒的にZeroFormatterがよい。

ソースコードが1行で済むけど速度が遅くなるなら
コード量は増えてもいいと思っているけど、この結果ならFlatBuffersを採用するメリットはほぼない。

前回のEasySaveとの比較と同じ端末使ったけども
EasySaveは明らかに遅かった。(FlatBuffersの2倍遅いので、ZeroFormatterの2倍遅いともいえる)
ZeroFormatterはC#コードの自動生成のためコンパイル処理が必要。
EasySaveはアセットをインポートすればすぐ使える。
速度を意識しないのであれば、そのあたりはお好みで。

ZeroFormatterでデータがかけてたとき(AとBのBしか書き込まれてなかったとき)の挙動とか細かい仕様が分からないので
そのあたり、誰か調査してー。