PG日誌

各記事はブラウザの横幅を1410px以上にすると2カラムの見出しが表示されます。なるべく横に広げてみてください。

【Unity】JSONのシリアライズ・デシリアライズの性能比較

Unity環境で使用できる JSON をシリアライズ・デシリアライズできるライブラリとパフォーマンスを調査したいと思います。

はじめに

実際はこのネタ使い古しなので先人よるパフォーマンス計測はいくらでもされているので実際は以下サイトを参考にしたほうが効率がいいと思います…

https://developpaper.com/unity-json-performance-comparison-litjson-newton-softjson-simplejson/

https://github.com/neuecc/Utf8Json

Unity で使用できるJSONライブラリ

標準機能を合わせて以下の4つが有力かと思います。

というか クラス ⇔ JSON を簡単に直接変換できる仕組みを検証します。SimpleJSONはJSONオブジェクト型にパースがされるので高速ですがここでは除外します。だって面倒なんだもん…、そしてutf8jsonはパフォーマンスは素晴らしいですが IL2CPP 環境下で問題が出た時に責任取れないのでここでは計測除外します。扱えるようになったらいつか計測してみたいかなと。

種類 説明
JSON Utility Unity が提供するライブラリ。軽い代わりに制約多数。
DataContractJsonSerializer .NET 標準でかなり昔から使える
System.Text.Json .NET Standard2.1~なので2020以降で使用可能
JSON.NET .NET で有力な 3rd 製ライブラリで機能が豊富

JSON Utility はパフォーマンスが良いのですが、Dictionary 型をシリアル化できない、リストを直接シリアル化できないなどなどほかにもいっぱい色々あるのですが制約が多いです(おそらくサポート範囲がSerializableでインスペクター上で扱えるルール以外全部仕様を落としているという感じです)そこでほかの一般的なライブラリを使用しようとすると制約がない代わりに処理が重いなど一長一短があるようです。そのための参考として比較を行いたいと思います。

System.Text.Json は割と軽くて使いやすいですが現行の Unity の環境で2020を使用している人が多分ほぼいないと思うので検査から除外しています。除外だらけw

レギュレーション

確認環境

  • Unity 2019.4.7f1
  • Windows10
  • VisualStudio2019

補足:

計測はUnityのプロファイラーを使用する

使用するJSON型

以前 DataContractJsonSerializer の使い方で使用した以下のJSON形式を使用します。

このデータを生成して各ライブラリでシリアライズ・デシリアライズしてパフォーマンスを計測していきたいと思います。

[
    {
        "id": 0,
        "name": "Taka",
        "numbers": [
            0,
            1,
            2,
            3
        ],
        "list": [
            0,
            1,
            2,
            3
        ],
        "map": [
            {
                "Key": "key1",
                "Value": "value1"
            },
            {
                "Key": "key2",
                "Value": "value2"
            },
            {
                "Key": "key3",
                "Value": "value3"
            }
        ]
    },
    {
        "id": 1,
        "name": "PG",
        ...// 以下繰り返し
]

使用するクラスの定義

公平を期すためにすべてのテストで以下の定義を使用します。

// 親のクラス
[DataContract]
[Serializable]
[JsonObject]
public class JsonParent
{
    [DataMember(Name = "items")]
    [JsonProperty("items")]
    public List<JsonItem> list = new List<JsonItem>();
}

// 子クラス
[DataContract]
[Serializable]
[JsonObject]
public class JsonItem
{
    [DataMember(Name = "id")]
    [JsonProperty("id")]
    public int id;

    [DataMember(Name = "name")]
    [JsonProperty("name")]
    public string name;

    [DataMember(Name = "numbers")]
    [JsonProperty("numbers")]
    public int[] numbers;

    [DataMember(Name = "list")]
    [JsonProperty("list")]
    public List<float> numberList = new List<float>();

    //[DataMember(Name = "map")]
    //[JsonProperty("map")]
    //public IDictionary<string, string> attributes = new Dictionary<string, string>();
}

測定結果

補足:

公平を期すためにDictionaryの部分はコメントアウトしています。

1件を100回シリアライズ・デイシリアライズ

f:id:Takachan:20200816194119p:plain

シリアライズ
Lib Time GC Alloc 備考
JsonUtility 2.12ms 54.8KB
DataContractJsonSerializer 447.35ms 1.5MB 突出して遅い
JSON.NET 205.67ms 0.7MB
デシリアライズ
Lib Time GC Alloc 備考
JsonUtility 6.7ms 39.2KB
DataContractJsonSerializer 288.6ms 1.9MB 突出して遅い
JSON.NET 81.59ms 415.1KB

10件を100回シリアライズ・デシリアライズ

f:id:Takachan:20200816194716p:plain

シリアライズ
Lib Time GC Alloc 備考
JsonUtility 6.33ms 491.9KB
DataContractJsonSerializer 1885.52ms 7.2MB 突出して遅い、件数増加で悪化
JSON.NET 594.06ms 3.0MB
デシリアライズ
Lib Time GC Alloc 備考
JsonUtility 31.65ms 301.1KB
DataContractJsonSerializer 2006.74ms 4.9MB 突出して遅い、件数増加で悪化
JSON.NET 448.21ms 1.4MB

100件を100回シリアライズ・デシリアライズ

f:id:Takachan:20200816200554p:plain

シリアライズ

| Lib | Time | GC Alloc | 備考 |-|-| | JsonUtility | 45.69ms | 4.8MB | | DataContractJsonSerializer | 11569.97ms | 62.8MB | 突出して遅い、件数増加で悪化 | JSON.NET | 2170.23ms | 22.7MB |

デシリアライズ
Lib Time GC Alloc 備考
JsonUtility 115.29ms 2.9MB
DataContractJsonSerializer 15389.79ms 35.4MB 突出して遅い、件数増加で悪化
JSON.NET 2921.73ms 11.4MB

1000件を1回シリアライズ・デシリアライズ

f:id:Takachan:20200816201054p:plain

シリアライズ
Lib Time GC Alloc 備考
JsonUtility 5.12ms 490.3KB
DataContractJsonSerializer 1320.66ms 6.7MB 突出して遅い
JSON.NET 491.31ms 2.5MB
デシリアライズ
Lib Time GC Alloc 備考
JsonUtility 9.97ms 291.8KB
DataContractJsonSerializer 1529.85ms 3.4MB 突出して遅い
JSON.NET 302.28ms 1.1MB

結論

  • 速度が必要な場所では JsonUtility を使用する
    • 毎フレーム処理するような場合確実にこれを選択する以外ない
  • 汎用性の方が大切な場合 Json.NET を使用する
    • ロード中などの余裕のある時に複雑なデータ処理をするときはこっち
  • DataContractJsonSerializer は論外(ゲームどころかあらゆるケースで使用NG)
    • 絶対に使用しないこと

測定コード

以下のコードを実行してプロファイラーから確認しました。

JsonTestクラス

適当なゲームオブジェクトにアタッチして起動時に検査コードを実行する処理を行います。

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using UnityEngine;

public class JsonTest : MonoBehaviour
{
    // 生成するデータ量
    [SerializeField] private int DataLength = 10;
    // シリアライズ・デシリアライズの試行回数
    [SerializeField] private int TestCount = 10;

    public void Start()
    {
        // あらかじめデータを生成しておいて
        JsonParent item = DataJenerator.CreateTestData(this.DataLength);

        // 全て同じデータで確認する
        this.RunJsonUtility(item);
        this.RunJsonStd(item);
        this.RunJSOdotNET(item);
    }

    public void RunJsonUtility(JsonParent item)
    {
        for (int i = 0; i < TestCount; i++)
        {
            string jsonStr = JsonUtility.ToJson(item);
            JsonParent _tmp = JsonUtility.FromJson<JsonParent>(jsonStr);

            if (i == 0)
            {
                UnityEngine.Debug.Log($"JsonUtil={jsonStr}");
            }
        }
    }

    public void RunJsonStd(JsonParent item)
    {
        for (int i = 0; i < TestCount; i++)
        {
            string jsonStr = JsonUtilityStd.Serialize(item);
            JsonUtilityStd.Deserialize<JsonParent>(jsonStr);

            if (i == 0)
            {
                UnityEngine.Debug.Log($"JsonUtilityStd={jsonStr}");
            }
        }
    }

    public void RunJSOdotNET(JsonParent item)
    {
        for (int i = 0; i < TestCount; i++)
        {
            string jsonStr = JsonConvert.SerializeObject(item);
            JsonConvert.DeserializeObject<JsonParent>(jsonStr);

            if (i == 0)
            {
                UnityEngine.Debug.Log($"RunJSOdotNET={jsonStr}");
            }
        }
    }
}

DataJeneratorクラス

検査対象のダミーデータを生成します。

public static class DataJenerator
{
    // 指定した件数分のJSONに変換するデータを生成する
    public static JsonParent CreateTestData(int count)
    {
        IEnumerable<JsonItem> f()
        {
            for (int i = 0; i < count; i++)
            {
                var item = new JsonItem()
                {
                    id = UnityEngine.Random.Range(0, int.MaxValue),
                    name = Guid.NewGuid().ToString(),
                    numbers = new int[]
                    {
                        UnityEngine.Random.Range(0, int.MaxValue),
                        UnityEngine.Random.Range(0, int.MaxValue),
                        UnityEngine.Random.Range(0, int.MaxValue),
                        UnityEngine.Random.Range(0, int.MaxValue),
                        UnityEngine.Random.Range(0, int.MaxValue),
                    },
                };

                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));
                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));
                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));
                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));
                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));

                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();

                yield return item;
            }
        }

        return new JsonParent()
        {
            list = f().ToList(),
        };
    }
}

JsonUtilityStdクラス

DataContractJsonSerializer の汎用処理をサポートします。

public static class JsonUtilityStd
{
    public static string Serialize(object graph)
    {
        using (var stream = new MemoryStream())
        {
            var serializer = new DataContractJsonSerializer(graph.GetType());
            serializer.WriteObject(stream, graph);
            return Encoding.UTF8.GetString(stream.ToArray());
        }
    }
    public static T Deserialize<T>(string message)
    {
        using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(message)))
        {
            //var setting = new DataContractJsonSerializerSettings()
            //{
            //    UseSimpleDictionaryFormat = true,
            //};
            var serializer = new DataContractJsonSerializer(typeof(T)/*, setting*/);
            return (T)serializer.ReadObject(stream);
        }
    }
}

参考資料

各ライブラリの特

いちおう各ライブラリの特徴を書いておきます。

  • JSON Utility
    • 長所
      • とにかく動作が軽い・早い
      • Unityで動作保証されている
    • 短所
      • オブジェクトの名前が指定できない
      • 制約が多いのでオブジェクトの変換などを事前にする必要がある
        • プロパティが対象にならない
        • Dictionary型が使えない
        • List型をルートに使用できない
        • DateTime型が変換できない
        • etc...
  • DataContractJsonSerializer
    • 長所
      • ほぼすべてのオブジェクトを変換できる
        • Listを直接変換できる, Dictionaryも余裕で扱える
      • オブジェクト名が指定できる
      • Unityに依存していないので汎用性が高い(Unity外で制作も可能)
      • 雑に使ってもたいてい動く
    • 短所
      • あらかじめクラスを定義する必要がある
      • ルート要素に名前が付けられない
      • 死ぬほど動作が重い(1000倍くらい重い)
        • 汎用性が非常に高い分動作が低速
  • JSON.NET
    • 長所
      • 機能的に一番汎用性が高い
        • "DataContractJsonSerializer" の長所をすべて持っている
          • ルート要素にも名前が付けられる
      • Unityに依存していないので汎用性が高い(Unity外で制作も可能)
      • あらかじめオブジェクトを作成しないでもJsonObjectという汎用的な型で扱える
    • 短所
      • 割と処理は重い

各ライブラリのシリアライズ後の文字列

参考までに各ライブラリで作成したJSONを以下に記載します。ライブラリごとにクセがめちゃくちゃ色々あるのでよく注意したほうがいいです。特に違うシステムと連携したときにパース出来ないとか意図しない形式にシリアル化されているとか、逆に仕様書通りに出力できないとかマジで色々あるので特徴と機能はよく理解しておく必要がありそうです。

Json Utilityu

Dictionary型は無視されています。というかエンジンの仕様で変換できないメンバーが無視して出力しません。

{
    "list": [
        {
            "id": 1447338837,
            "name": "9b3b9b8c-9b5c-4976-8ca6-18906b1322f1",
            "numbers": [
                712401974,
                328545571,
                1454266954,
                629343681,
                1011712660
            ],
            "numberList": [
                1.3944311762943991e+38,
                2.8733568427566508e+38,
                3.3132977216911543e+38,
                3.1354838649369004e+38,
                1.2534365608129303e+37
            ]
        }
    ]
}
DataContractJsonSerializer

Dictionaryは Key - Value で自動的に整形されて出力されます。順序保証がされないので並び順も適当です。

{
    "items": [
        {
            "id": 840029637,
            "list": [
                3.07462284e+38,
                1.84336315e+38,
                2.1545555e+37,
                1.25667769e+37,
                1.46380977e+37
            ],
            "map": [
                {
                    "Key": "f0f37831-501f-452b-a4c4-f7e823f1b71f",
                    "Value": "aa59fcd5-a01b-4c66-a9cd-e183d80e6df6"
                },
                {
                    "Key": "50b95a9c-d3ee-4ea0-a839-2c57599d7612",
                    "Value": "c8cb5110-24aa-4a3f-a9ec-0d035b3bddad"
                },
                {
                    "Key": "e1072425-95ce-4aaf-9bf1-2bd5dede3ff2",
                    "Value": "2c1fee2f-d3e0-41d0-8a4d-314b94b094ef"
                },
                {
                    "Key": "ccbbadb8-20a5-4358-be64-67756f72e893",
                    "Value": "8618a4ad-adb0-4f89-a5f2-db7729c3ef27"
                },
                {
                    "Key": "4d08761c-d9c3-4e9c-87c6-dafa7b8e65df",
                    "Value": "15c67586-5c92-4d13-8f34-5b254ef259fb"
                }
            ],
            "name": "65868ff1-507c-4694-8abc-673bb108a3eb",
            "numbers": [
                2112028652,
                1602046542,
                1331859955,
                1935792680,
                420890169
            ]
        }
    ]
}
Json.NET

一番JSONっぽい出力になります。順序も宣言順を守ってくれます。

{
    "items": [
        {
            "id": 840029637,
            "name": "65868ff1-507c-4694-8abc-673bb108a3eb",
            "numbers": [
                2112028652,
                1602046542,
                1331859955,
                1935792680,
                420890169
            ],
            "list": [
                3.07462284e+38,
                1.84336315e+38,
                2.1545555e+37,
                1.25667769e+37,
                1.46380977e+37
            ],
            "map": {
                "f0f37831-501f-452b-a4c4-f7e823f1b71f": "aa59fcd5-a01b-4c66-a9cd-e183d80e6df6",
                "50b95a9c-d3ee-4ea0-a839-2c57599d7612": "c8cb5110-24aa-4a3f-a9ec-0d035b3bddad",
                "e1072425-95ce-4aaf-9bf1-2bd5dede3ff2": "2c1fee2f-d3e0-41d0-8a4d-314b94b094ef",
                "ccbbadb8-20a5-4358-be64-67756f72e893": "8618a4ad-adb0-4f89-a5f2-db7729c3ef27",
                "4d08761c-d9c3-4e9c-87c6-dafa7b8e65df": "15c67586-5c92-4d13-8f34-5b254ef259fb"
            }
        }
    ]
}