いろいろなフォーマットのデシリアライズ速度を計測してみた#
Unity5.3でJSONの公式サポートが実装されました!
https://unity3d.com/jp/unity/whats-new/unity-5.3
というわけでいろんなフォーマットはあるけれど簡単に実装したいときにどれが一番高速なの?やっぱり公式強いの?
と気になったので、計測してみました。
実験コードは↓になげておきました。
ご自由にお使いください
https://github.com/Marimoiro/UnityDeserializeTimes
| 方法 | フォーマット | 備考 | 
|---|---|---|
| Resources.Load | ScriptableObject | ロードのみ | 
| JsonUtility | Json | Unity5.3から追加 | 
| JsonFx | Json | Jsonライブラリのひとつ | 
| YamlDotNet | Yaml | Yamlパーサ | 
| XmlSerializer | Xml | C#標準のxmlパーサ | 
結論としてはJsonUtilityとResources.Load使っとけってなりました
準備#
対象データ##
対象データは以下の二つです。
内部にクラス持ってると速度差でるかなーとか思って二種類用意しました。
using System;
using UnityEngine;
using System.Collections;
[Serializable]
public class SimpleParams
{ 
    public int X;
    public int XProperty
    {
        get { return X; }
        set { X = value; }
    }
    public double Y;
    public double YProperty
    {
        get { return Y; }
        set { Y = value; }
    }
    public string Message;
    public string MessageProperty
    {
        get { return Message; }
        set { Message = value; }
    }
    public override bool Equals(object obj)
    {
        var o = obj as SimpleParams;
        if (o == null) return false;
        return Equals(o);
    }
    protected bool Equals(SimpleParams other)
    {
        return X == other.X && Math.Abs(Y - other.Y) < 0.001 && string.Equals(Message, other.Message);
    }
    public override int GetHashCode()
    {
        unchecked
        {
            var hashCode = X;
            hashCode = (hashCode*397) ^ (Message != null ? Message.Length : 0);
            return hashCode;
        }
    }
}
using System;
using UnityEngine;
using System.Collections;
[Serializable]
public class NestParams
{
    public SimpleParams Params;
    public SimpleParams SimpleProperty {get { return Params; }set { Params = value; } }
    public string Message;
    public string MessageProperty { get { return Message; } set { Message = value; } }
    public override bool Equals(object obj)
    {
        return Equals(obj as NestParams);
    }
    protected bool Equals(NestParams other)
    {
        if (other == null) return false;
        return Equals(Params, other.Params) && string.Equals(Message, other.Message);
    }
    public override int GetHashCode()
    {
        unchecked
        {
            return ((Params != null ? Params.GetHashCode() : 0)*397) ^ (Message != null ? Message.GetHashCode() : 0);
        }
    }
}
public class NestArray
{
    public NestParams[] NestParams;
    public NestParams[] NestParamsProperty
    {
        get { return NestParams; }
        set { NestParams = value; }
    }
}
注意点
- Equalsなどは正しく読めているかのチェックのためReSharperちゃんが出してくれた適当なやつ(今考えたらAllでよかった)
- プロパティを宣言しているのはYamlDotNetがフィールドをサポートしてないっぽいため
データ作成##
こんなカスタムエディタ作って10000この配列にしました。
using UnityEngine;
using System.Collections;
using System.Linq;
using UnityEditor;
public class CreateAssetEditor : MonoBehaviour
{
    const int Samples = 10000;
    [MenuItem("Assets/Create/SampleObjects")]
    static void CreateSampleAsset()
    {
        var simples = new int[Samples].Select(_ => new SimpleParams()
        {
            Message = "TestMessage",
            X = Random.Range(0, 10000),
            Y = Random.Range(0, 1f)
        }).ToArray();
        var nests = simples.Select(s => new NestParams() {Message = "NestParam", Params = s}).ToArray();
        var sa = ScriptableObject.CreateInstance<SimpleObject>();
        sa.SimpleParamses = simples;
        AssetDatabase.CreateAsset(sa,"Assets/Resources/simples.asset");
        var na = ScriptableObject.CreateInstance<NestObject>();
        na.NestParams = nests;
        AssetDatabase.CreateAsset(na, "Assets/Resources/nests.asset");
        AssetDatabase.SaveAssets();
    }
}
SimpleObject/NestObjectはSimpleParamsとNestParamsの配列を宣言してあるだけのオブジェクトです。
YamlDotNetが配列そのままシリアライズをサポートしてなかった…
計測方法#
Cubeを作ってそこにぺたぺたパーサをはっていくかんじにしました。
パーサのベースクラス
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public interface IParser
{
    IList<int> SerializeTimeList { get; }
    IList<int> DeserializeTimeList { get; }
    string Serialize<T>(T obj);
    T Deserialize<T>(string text);
}
public class ParserBase : MonoBehaviour,IParser
{
    private readonly IList<int> serializeTimeList = new List<int>();
    private readonly IList<int> deserializeTimeList = new List<int>();
    public IList<int> SerializeTimeList
    {
        get { return serializeTimeList; }
    }
    public IList<int> DeserializeTimeList
    {
        get { return deserializeTimeList; }
    }
    public virtual string Serialize<T>(T obj)
    {
        throw new System.NotImplementedException();
    }
    public virtual T Deserialize<T>(string text)
    {
        throw new System.NotImplementedException();
    }
}
これを継承してたとえばUnityのJsonはこんな感じで時間を計ってます。
インスタンス生成やらの時間は考慮に入れないためにシリアライズ部分だけでの計測にしています。
using System;
using UnityEngine;
using System.Collections;
public class UnityJsonParser : ParserBase {
    public override string Serialize<T>(T obj)
    {
        var s = Environment.TickCount;
        var r = JsonUtility.ToJson(obj);
        SerializeTimeList.Add(Environment.TickCount - s);
        return r;
    }
    public override T Deserialize<T>(string text)
    {
        var s = Environment.TickCount;
        var r = JsonUtility.FromJson<T>(text);
        DeserializeTimeList.Add(Environment.TickCount - s);
        return r;
    }
}
実際に計測するクラスはこんな感じで適当にScriptableObjectだけは大本だしそもそもデシリアライズできないしなので、別扱いしてます。
using System;
using UnityEngine;
using System.Collections;
using System.Linq;
public class SamplingMaster : MonoBehaviour {
	// Use this for initialization
	void Start ()
	{
        //ScriptableObjectだけ
	    var start = Environment.TickCount;
	    var simples = new SimpleArray() {SimpleParams = Resources.Load<SimpleObject>("simples").SimpleParamses};
	    var sl = Environment.TickCount - start;
        start = Environment.TickCount;
	    var nest = new NestArray() {NestParams = Resources.Load<NestObject>("nests").NestParams};
	    var nl = Environment.TickCount - start;
	    OutputData(typeof (ScriptableObject), sl, nl, 0, 0);
        
	    var parsers = GetComponents<IParser>();
	    foreach (var parser in parsers)
	    {
	        var sText = parser.Serialize(simples);
	        var nText = parser.Serialize(nest);
	        var sObj = parser.Deserialize<SimpleArray>(sText);
	        var nObj = parser.Deserialize<NestArray>(nText);
	        OutputData(parser.GetType(), parser.DeserializeTimeList[0], parser.DeserializeTimeList[1], parser.SerializeTimeList[0], parser.SerializeTimeList[1]);
            ValidSimpleParams(simples,sObj);
            ValidNestParams(nest,nObj);
	    }
	}
    void OutputData(Type parserType, int simpleLoadTime, int nestLoadTime, int simpleWriteTime, int nestWriteTime)
    {
        Debug.LogFormat("{0} \r\n シリアライズ\r\n  Simple:{1}ms \r\n  Nest:{2}ms \r\n デシリアライズ\r\n  Simple:{3}ms \r\n  Nest:{4}ms",
            parserType, simpleWriteTime, nestWriteTime, simpleLoadTime, nestLoadTime);
    }
    void ValidSimpleParams(SimpleArray expect,SimpleArray actual)
    {
        var err = expect.SimpleParams.Select((e, n) => new {E = e, A = actual.SimpleParams[n],N = n}).FirstOrDefault(p => !p.E.Equals(p.A));
        if (err == null)
        {
            Debug.Log("Valid OK[Simple]");
        }
        else
        {
            Debug.Log("Error");
        }
    }
    void ValidNestParams(NestArray expect, NestArray actual)
    {
        var err = expect.NestParams.Select((e, n) => new { E = e, A = actual.NestParams[n], N = n }).FirstOrDefault(p => !p.E.Equals(p.A));
        if (err == null)
        {
            Debug.Log("Valid OK[Nest]");
        }
        else
        {
            Debug.Log("Error");
        }
    }
}
結果#
結果以下のようなデータになりました。(数値の単位はms)
すべてエディターでの計測です。
| 方法 | Simpleデシリアライズ | Nestデシリアライズ | Simpleシリアライズ | Nestシリアライズ | 
|---|---|---|---|---|
| Resources.Load | 16 | 47 | - | - | 
| JsonUtility | 16 | 31 | 16 | 15 | 
| JsonFx | 1529 | 3616 | 500 | 1107 | 
| YamlDotNet | 3026 | 4743 | 2730 | 4368 | 
| XmlSerializer | 1389 | 3354 | 827 | 1935 | 
参考までに別Windowsでやった場合を載せておきます(誰か追試してほしいな)
| 方法 | Simpleデシリアライズ | Nestデシリアライズ | Simpleシリアライズ | Nestシリアライズ | 
|---|---|---|---|---|
| Resources.Load | 32 | 31 | - | - | 
| JsonUtility | 15 | 32 | 0 | 16 | 
| JsonFx | 1188 | 3125 | 500 | 1062 | 
| YamlDotNet | 2766 | 4250 | 2438 | 3812 | 
| XmlSerializer | 1109 | 2813 | 578 | 1406 | 
所感#
Resources.LoadはともかくJsonUtilityがべらぼうに速いです。
ResourcesLoadだけはファイルの読み込みもかかっているのでもう少しデシリアライズだけだと速いはず。(分けられないんですけどね)
JsonUtilityの制限はおそらくScriptableObjectと同じのようです。
なのでDictionaryなんかは使えないっぽい。
この結果だけ見ると固定データはScriptableObjectかJson,動的データはJsonにしてUnity公式のものを使うのがよいっぽい。
Yaml/Xmlはそれぞれデシリアライズの入力にTextReaderをとってstringを受け付けてくれない。その分のオーバーヘッドかかっているのかなぁというような気がします。
またResources.Loadは起動後最初のロードで若干のオーバーヘッドがあります、