Unity
FlatBuffers
ZeroFormatter

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

More than 1 year has passed since last update.


内容

ローカルにデータを保存するときにZeroFormatterとFlatBuffersのどちらが速いか比較検討する。

この手のテストは書き込むデータタイプによって偏りがでるので公平性は無視して

ある程度、普段使いしそうなデータを書き込む。

(I/Oの速度もあるしテストする機によってもアレだしね :innocent:)

データタイプは前回使ったデータタイプを使用する。


環境

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


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しか書き込まれてなかったとき)の挙動とか細かい仕様が分からないので

そのあたり、誰か調査してー。