💾

埋め込み型 JSON シリアライザー

に公開

Jsonable は埋め込み型の JSON シリアライザージェネレーターで、読み書きの際にシリアライザーを経由せず myObj.ToJson() で直接 JSON を出力することが出来ます。呼び出し側にシリアライザーへの依存が不要なのが利点でもあり欠点でもあります。

読み書きのパフォーマンスは Json.NET や System.Text.Json(ソースジェネレーター)より速く、MessagePack for C# より遅いが配列の再利用モードを有効にすると最速になるケースもあるって感じです。

パフォーマンス比較結果

MessagePack Json.NET

書き出し

using Jsonable;

[ToJson]
partial class MyData  // struct, record も可
{
    public int Value { get; set; }
}
var data = new MyData();
data.ToJson();

// UTF-8
var writer = new ArrayBufferWriter<byte>();
data.ToJsonUtf8(writer);

パフォーマンス

テストデータはざっくり

  • Citm_Catalog: 構造が複雑だが空配列が多い
    • events = 184 個
    • performances = 243 個
  • Twitter: 構造が単純で文字列が多い
    • statuses = 100 個
  • performance/statuses: 1エントリー = JSON 100 行超

です。

ToJsonUtf8CacheArrayBufferWriter<byte> インスタンスを使いまわしているので、見えていないだけで1回分のアロケーションはあるハズです。

キャッシュ無しの ToJsonUtf8 との差を見ると配列の確保は割と時間がかかる処理なのが良く分かります。

Citm_Catalog [1]

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Save_ToJsonUtf8Cache 1 0.6114 ms 1.00 0 MB 1.00
Catalog_Save_MsgPack 1 0.4738 ms 0.77 0.33 MB 891.99
Catalog_Save_SysTxtJson 1 0.6918 ms 1.13 0.96 MB 2,610.18
Catalog_Save_SysTxtJsonUtf8 1 0.5419 ms 0.89 - 0.00
Catalog_Save_JsonNET 1 3.3763 ms 5.52 2.57 MB 7,018.59
Catalog_Save_ToJsonUtf8 1 0.8033 ms 1.31 1.72 MB 4,695.10
Catalog_Save_ToJsonable 1 1.4144 ms 2.31 1.72 MB 4,694.87
Catalog_Save_ToJson 1 0.9381 ms 1.53 2.67 MB 7,300.44

Twitter [1:1]

Method Boost Mean Ratio Allocated Alloc Ratio
Twitter_Save_ToJsonUtf8Cache 1 0.5457 ms 1.00 0.28 MB 1.00
Twitter_Save_MsgPack 1 0.4842 ms 0.89 0.39 MB 1.40
Twitter_Save_SysTxtJson 1 0.8105 ms 1.49 1.11 MB 4.00
Twitter_Save_SysTxtJsonUtf8 1 0.6099 ms 1.12 - 0.00
Twitter_Save_JsonNET 1 1.7391 ms 3.19 1.74 MB 6.27
Twitter_Save_ToJsonUtf8 1 0.7680 ms 1.41 1.73 MB 6.24
Twitter_Save_ToJsonable 1 0.9655 ms 1.77 1.73 MB 6.24
Twitter_Save_ToJson 1 1.0194 ms 1.87 2.54 MB 9.19
配列サイズ10倍

Citm_Catalog

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Save_ToJsonUtf8Cache 10 5.8673 ms 1.00 0 MB 1.00
Catalog_Save_MsgPack 10 4.5922 ms 0.78 3.24 MB 884.64
Catalog_Save_SysTxtJson 10 7.6414 ms 1.30 9.49 MB 2,590.00
Catalog_Save_SysTxtJsonUtf8 10 5.1799 ms 0.88 - 0.00
Catalog_Save_JsonNET 10 38.7559 ms 6.61 25.5 MB 6,958.65
Catalog_Save_ToJsonUtf8 10 7.5830 ms 1.29 13.75 MB 3,753.73
Catalog_Save_ToJsonable 10 13.9980 ms 2.39 27.5 MB 7,506.49
Catalog_Save_ToJson 10 9.3162 ms 1.59 23.24 MB 6,341.68

Twitter

Method Boost Mean Ratio Allocated Alloc Ratio
Twitter_Save_ToJsonUtf8Cache 10 5.5676 ms 1.00 2.77 MB 1.00
Twitter_Save_MsgPack 10 5.1590 ms 0.93 3.88 MB 1.40
Twitter_Save_SysTxtJson 10 8.8821 ms 1.60 11.06 MB 4.00
Twitter_Save_SysTxtJsonUtf8 10 6.0311 ms 1.08 - 0.00
Twitter_Save_JsonNET 10 17.8067 ms 3.20 17.25 MB 6.23
Twitter_Save_ToJsonUtf8 10 8.9360 ms 1.61 14.37 MB 5.19
Twitter_Save_ToJsonable 10 10.8424 ms 1.95 14.37 MB 5.19
Twitter_Save_ToJson 10 12.9677 ms 2.33 22.63 MB 8.18

Unity

Unity 想定の .NET 5(ベンチマークの都合)は露骨に結果が悪くなっていますが、おそらく文字列とスパン周りの最適化が .NET 9 と比べて劣っていることが原因だと思われます。

.NET 5(Unity 想定)

Citm_Catalog

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Save_ToJsonUtf8Cache 1 1.2293 ms 1.00 0 MB 1.00
Catalog_Save_MsgPack 1 0.7269 ms 0.59 0.33 MB 792.83
Catalog_Save_SysTxtJson 1 1.1094 ms 0.90 0.96 MB 2,319.97
Catalog_Save_SysTxtJsonUtf8 1 0.7336 ms 0.60 - 0.00
Catalog_Save_JsonNET 1 5.9307 ms 4.82 2.57 MB 6,239.01
Catalog_Save_ToJsonUtf8 1 1.7808 ms 1.45 1.72 MB 4,172.83
Catalog_Save_ToJsonable 1 2.2965 ms 1.87 1.72 MB 4,172.89
Catalog_Save_ToJson 1 2.3596 ms 1.92 2.67 MB 6,488.34

Twitter

Method Boost Mean Ratio Allocated Alloc Ratio
Twitter_Save_ToJsonUtf8Cache 1 1.1330 ms 1.00 0.3 MB 1.000
Twitter_Save_MsgPack 1 0.7275 ms 0.64 0.39 MB 1.309
Twitter_Save_SysTxtJson 1 1.2422 ms 1.10 1.11 MB 3.728
Twitter_Save_SysTxtJsonUtf8 1 0.8375 ms 0.74 0 MB 0.000
Twitter_Save_JsonNET 1 2.4439 ms 2.16 1.74 MB 5.841
Twitter_Save_ToJsonUtf8 1 1.5669 ms 1.38 1.75 MB 5.878
Twitter_Save_ToJsonable 1 2.0776 ms 1.83 1.75 MB 5.887
Twitter_Save_ToJson 1 2.2839 ms 2.02 2.55 MB 8.573

👇 配列サイズ10倍

Citm_Catalog

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Save_ToJsonUtf8Cache 10 12.2460 ms 1.00 0 MB 1.00
Catalog_Save_MsgPack 10 7.9068 ms 0.65 3.24 MB 786.03
Catalog_Save_SysTxtJson 10 15.7871 ms 1.29 23.49 MB 5,696.32
Catalog_Save_SysTxtJsonUtf8 10 7.2894 ms 0.60 - 0.00
Catalog_Save_JsonNET 10 61.8554 ms 5.05 25.5 MB 6,183.02
Catalog_Save_ToJsonUtf8 10 15.9628 ms 1.30 13.75 MB 3,335.43
Catalog_Save_ToJsonable 10 26.9025 ms 2.20 27.5 MB 6,669.83
Catalog_Save_ToJson 10 20.2645 ms 1.65 23.24 MB 5,634.91

Twitter

Method Boost Mean Ratio Allocated Alloc Ratio
Twitter_Save_ToJsonUtf8Cache 10 11.4026 ms 1.00 2.97 MB 1.000
Twitter_Save_MsgPack 10 7.5894 ms 0.67 3.88 MB 1.308
Twitter_Save_SysTxtJson 10 16.7585 ms 1.47 25.06 MB 8.437
Twitter_Save_SysTxtJsonUtf8 10 8.6035 ms 0.75 0 MB 0.000
Twitter_Save_JsonNET 10 26.5548 ms 2.33 17.25 MB 5.808
Twitter_Save_ToJsonUtf8 10 15.7008 ms 1.38 14.58 MB 4.907
Twitter_Save_ToJsonable 10 18.7277 ms 1.64 14.97 MB 5.040
Twitter_Save_ToJson 10 18.9953 ms 1.67 22.81 MB 7.678

読み込み

using Jsonable;

[FromJson]
partial class MyData  // struct, record も可
{
    public int Value { get; set; }
}
var data = new MyData();
data.FromJsonable(jsonc_utf8, reuseInstance: false);  // 配列やオブジェクトの再利用(後述)

パフォーマンス

ToJsonable は書き出し速度で Syste.Text.Json に劣りますが、その分読み込みが爆速になり、読み込んだ結果以外のメモリ消費がほぼ消滅します。

なお、Jsonable の読み込み機能は .jsonc フォーマットに限るという、何とも言えない制約があります。

Citm_Catalog

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Load_FromJsonable 1 0.9457 ms 1.00 0.55 MB 1.00
Catalog_Load_MsgPack 1 0.7654 ms 0.81 0.55 MB 1.00
Catalog_Load_FromJsonableReuseArray 1 0.8810 ms 0.93 0.09 MB 0.16
Catalog_Load_FromJsonableReuseList 1 0.9192 ms 0.97 0.54 MB 0.98
Catalog_Load_SysTxtJson 1 4.9116 ms 5.19 1.15 MB 2.08
Catalog_Load_SysTxtJsonUtf8 1 7.4355 ms 7.86 1.15 MB 2.08
Catalog_Load_JsonNET 1 7.0533 ms 7.46 1.68 MB 3.05

Twitter

Method Boost Mean Ratio Allocated Alloc Ratio
Twitter_Load_FromJsonable 1 0.9166 ms 1.00 0.55 MB 1.00
Twitter_Load_MsgPack 1 0.8882 ms 0.97 0.49 MB 0.90
Twitter_Load_FromJsonableArray 1 0.9376 ms 1.02 0.43 MB 0.79
Twitter_Load_FromJsonableReuseList 1 0.8635 ms 0.94 0.55 MB 1.00
Twitter_Load_SysTxtJson 1 1.8318 ms 2.00 0.54 MB 0.99
Twitter_Load_SysTxtJsonUtf8 1 2.4017 ms 2.62 0.54 MB 0.99
Twitter_Load_JsonNET 1 2.9220 ms 3.19 0.61 MB 1.11
配列サイズ10倍

Citm_Catalog

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Load_FromJsonable 10 14.0057 ms 1.00 5.38 MB 1.00
Catalog_Load_MsgPack 10 9.1853 ms 0.66 5.38 MB 1.00
Catalog_Load_FromJsonableReuseArray 10 9.4582 ms 0.68 0.8 MB 0.15
Catalog_Load_FromJsonableReuseList 10 10.5262 ms 0.75 5.31 MB 0.99
Catalog_Load_SysTxtJson 10 36.9992 ms 2.64 11.31 MB 2.10
Catalog_Load_SysTxtJsonUtf8 10 45.4580 ms 3.25 11.31 MB 2.10
Catalog_Load_JsonNET 10 83.1167 ms 5.94 16.59 MB 3.08

Twitter

Method Boost Mean Ratio Allocated Alloc Ratio
Twitter_Load_FromJsonable 10 13.3592 ms 1.00 5.49 MB 1.00
Twitter_Load_MsgPack 10 10.0158 ms 0.75 4.93 MB 0.90
Twitter_Load_FromJsonableArray 10 8.9105 ms 0.67 4.34 MB 0.79
Twitter_Load_FromJsonableReuseList 10 9.6351 ms 0.72 5.48 MB 1.00
Twitter_Load_SysTxtJson 10 16.3961 ms 1.23 5.4 MB 0.98
Twitter_Load_SysTxtJsonUtf8 10 19.3463 ms 1.45 5.4 MB 0.98
Twitter_Load_JsonNET 10 29.1456 ms 2.18 5.94 MB 1.08

Unity

構造が単純な Citm_Catalog のパフォーマンスが良かった .NET 9 に比べて Twitter は露骨にパフォーマンスが低下しています。

(カタログは構造が複雑だけど空配列が多いので、配列読み込みメソッドの呼び出しがほぼ無い)

.NET 5(Unity 想定)

Citm_Catalog

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Load_FromJsonable 1 1.5598 ms 1.00 0.55 MB 1.00
Catalog_Load_MsgPack 1 1.3071 ms 0.84 0.55 MB 1.00
Catalog_Load_FromJsonableReuseArray 1 1.4540 ms 0.93 0.09 MB 0.16
Catalog_Load_FromJsonableReuseList 1 1.4987 ms 0.96 0.54 MB 0.98
Catalog_Load_SysTxtJson 1 8.2046 ms 5.26 2.79 MB 5.08
Catalog_Load_SysTxtJsonUtf8 1 11.0445 ms 7.08 1.15 MB 2.08
Catalog_Load_JsonNET 1 10.7300 ms 6.88 1.68 MB 3.05
Catalog_Load_FromJsonable 10 18.9356 ms 1.00 5.38 MB 1.00
Catalog_Load_MsgPack 10 14.0249 ms 0.74 5.38 MB 1.00
Catalog_Load_FromJsonableReuseArray 10 14.4595 ms 0.76 0.8 MB 0.15
Catalog_Load_FromJsonableReuseList 10 15.8106 ms 0.84 5.31 MB 0.99
Catalog_Load_SysTxtJson 10 63.8594 ms 3.37 16.05 MB 2.98
Catalog_Load_SysTxtJsonUtf8 10 77.3035 ms 4.08 11.31 MB 2.10
Catalog_Load_JsonNET 10 107.3388 ms 5.67 16.59 MB 3.08

👇 文字列データが多い Twitter の JSON

Method Boost Mean Ratio Allocated Alloc Ratio
Twitter_Load_FromJsonable 1 1.5990 ms 1.00 0.57 MB 1.00
Twitter_Load_MsgPack 1 0.9599 ms 0.60 0.49 MB 0.87
Twitter_Load_FromJsonableArray 1 1.5734 ms 0.98 0.45 MB 0.80
Twitter_Load_FromJsonableReuseList 1 1.6011 ms 1.00 0.57 MB 1.00
Twitter_Load_SysTxtJson 1 3.0483 ms 1.91 1.15 MB 2.01
Twitter_Load_SysTxtJsonUtf8 1 4.0812 ms 2.55 0.54 MB 0.96
Twitter_Load_JsonNET 1 4.3284 ms 2.71 0.61 MB 1.07
Twitter_Load_FromJsonable 10 20.0151 ms 1.00 5.69 MB 1.00
Twitter_Load_MsgPack 10 10.4810 ms 0.52 4.93 MB 0.87
Twitter_Load_FromJsonableArray 10 15.6526 ms 0.78 4.54 MB 0.80
Twitter_Load_FromJsonableReuseList 10 16.4685 ms 0.82 5.68 MB 1.00
Twitter_Load_SysTxtJson 10 27.5522 ms 1.38 9.94 MB 1.75
Twitter_Load_SysTxtJsonUtf8 10 34.8764 ms 1.74 5.4 MB 0.95
Twitter_Load_JsonNET 10 42.9746 ms 2.15 5.94 MB 1.04

テストデータ概要 [1:2]

Twitter テストデータ抜粋(長い)
{
  "statuses": [
    ...,
    {
      "metadata": {
        "result_type": "recent",
        "iso_language_code": "ja"
      },
      "created_at": "Sun Aug 31 00:28:56 +0000 2014",
      "id": 505874847260352513,
      "id_str": "505874847260352513",
      "text": "【マイリスト】【彩りりあ】妖怪体操第一 踊ってみた【反転】 http://t.co/PjL9if8OZC #sm24357625",
      "source": "<a href=\"http://www.nicovideo.jp/\" rel=\"nofollow\">ニコニコ動画</a>",
      "truncated": false,
      "in_reply_to_status_id": null,
      "in_reply_to_status_id_str": null,
      "in_reply_to_user_id": null,
      "in_reply_to_user_id_str": null,
      "in_reply_to_screen_name": null,
      "user": {
        "id": 1609789375,
        "id_str": "1609789375",
        "name": "食いしん坊前ちゃん",
        "screen_name": "2no38mae",
        "location": "ニノと二次元の間",
        "description": "ニコ動で踊り手やってます!!応援本当に嬉しいですありがとうございます!! ぽっちゃりだけど前向きに頑張る腐女子です。嵐と弱虫ペダルが大好き!【お返事】りぷ(基本は)”○” DM (同業者様を除いて)”×” 動画の転載は絶対にやめてください。 ブログ→http://t.co/8E91tqoeKX  ",
        "url": "http://t.co/ulD2e9mcwb",
        "entities": {
          "url": {
            "urls": [
              {
                "url": "http://t.co/ulD2e9mcwb",
                "expanded_url": "http://www.nicovideo.jp/mylist/37917495",
                "display_url": "nicovideo.jp/mylist/37917495",
                "indices": [
                  0,
                  22
                ]
              }
            ]
          },
          "description": {
            "urls": [
              {
                "url": "http://t.co/8E91tqoeKX",
                "expanded_url": "http://ameblo.jp/2no38mae/",
                "display_url": "ameblo.jp/2no38mae/",
                "indices": [
                  125,
                  147
                ]
              }
            ]
          }
        },
        "protected": false,
        "followers_count": 560,
        "friends_count": 875,
        "listed_count": 11,
        "created_at": "Sun Jul 21 05:09:43 +0000 2013",
        "favourites_count": 323,
        "utc_offset": null,
        "time_zone": null,
        "geo_enabled": false,
        "verified": false,
        "statuses_count": 3759,
        "lang": "ja",
        "contributors_enabled": false,
        "is_translator": false,
        "is_translation_enabled": false,
        "profile_background_color": "F2C6E4",
        "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/378800000029400927/114b242f5d838ec7cb098ea5db6df413.jpeg",
        "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/378800000029400927/114b242f5d838ec7cb098ea5db6df413.jpeg",
        "profile_background_tile": false,
        "profile_image_url": "http://pbs.twimg.com/profile_images/487853237723095041/LMBMGvOc_normal.jpeg",
        "profile_image_url_https": "https://pbs.twimg.com/profile_images/487853237723095041/LMBMGvOc_normal.jpeg",
        "profile_banner_url": "https://pbs.twimg.com/profile_banners/1609789375/1375752225",
        "profile_link_color": "FF9EDD",
        "profile_sidebar_border_color": "FFFFFF",
        "profile_sidebar_fill_color": "DDEEF6",
        "profile_text_color": "333333",
        "profile_use_background_image": true,
        "default_profile": false,
        "default_profile_image": false,
        "following": false,
        "follow_request_sent": false,
        "notifications": false
      },
      "geo": null,
      "coordinates": null,
      "place": null,
      "contributors": null,
      "retweet_count": 0,
      "favorite_count": 0,
      "entities": {
        "hashtags": [
          {
            "text": "sm24357625",
            "indices": [
              53,
              64
            ]
          }
        ],
        "symbols": [],
        "urls": [
          {
            "url": "http://t.co/PjL9if8OZC",
            "expanded_url": "http://nico.ms/sm24357625",
            "display_url": "nico.ms/sm24357625",
            "indices": [
              30,
              52
            ]
          }
        ],
        "user_mentions": []
      },
      "favorited": false,
      "retweeted": false,
      "possibly_sensitive": false,
      "lang": "ja"
    }
  ],
  "search_metadata": {
    "completed_in": 0.087,
    "max_id": 505874924095815700,
    "max_id_str": "505874924095815681",
    "next_results": "?max_id=505874847260352512&q=%E4%B8%80&count=100&include_entities=1",
    "query": "%E4%B8%80",
    "refresh_url": "?since_id=505874924095815681&q=%E4%B8%80&include_entities=1",
    "count": 100,
    "since_id": 0,
    "since_id_str": "0"
  }
}
Citm_Catalog テストデータ抜粋(長い)
{
    ...,
    "events": {
        "138586341": {
            "description": null,
            "id": 138586341,
            "logo": null,
            "name": "30th Anniversary Tour",
            "subTopicIds": [
                337184269,
                337184283
            ],
            "subjectCode": null,
            "subtitle": null,
            "topicIds": [
                324846099,
                107888604
            ]
        },
        ...
    },
    ...,
    "performances": [
        {
            "eventId": 138586341,
            "id": 339887544,
            "logo": null,
            "name": null,
            "prices": [
                {
                    "amount": 90250,
                    "audienceSubCategoryId": 337100890,
                    "seatCategoryId": 338937295
                },
                {
                    "amount": 66500,
                    "audienceSubCategoryId": 337100890,
                    "seatCategoryId": 338937296
                }
            ],
            "seatCategories": [
                {
                    "areas": [
                        {
                            "areaId": 205705999,
                            "blockIds": []
                        },
                        {
                            "areaId": 205705998,
                            "blockIds": []
                        },
                        {
                            "areaId": 205705994,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706006,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706005,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706004,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706003,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706002,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706007,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706009,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706008,
                            "blockIds": []
                        }
                    ],
                    "seatCategoryId": 338937295
                },
                {
                    "areas": [
                        {
                            "areaId": 205705999,
                            "blockIds": []
                        },
                        {
                            "areaId": 205705998,
                            "blockIds": []
                        },
                        {
                            "areaId": 205705994,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706006,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706005,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706004,
                            "blockIds": []
                        },
                        {
                            "areaId": 205705995,
                            "blockIds": []
                        },
                        {
                            "areaId": 205705996,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706003,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706002,
                            "blockIds": []
                        },
                        {
                            "areaId": 205705993,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706001,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706000,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706007,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706009,
                            "blockIds": []
                        },
                        {
                            "areaId": 205706008,
                            "blockIds": []
                        }
                    ],
                    "seatCategoryId": 338937296
                }
            ],
            "seatMapImage": null,
            "start": 1372701600000,
            "venueCode": "PLEYEL_PLEYEL"
        },
        ...
    ],
    ...
}

重いのは文字列処理

文字列処理が重しになっているのは明らかなので、試しに EscapeStringUnescapeString からエスケープが必要かの判定(utf8.IndexOfAny(...) >= 0)と、文字列のエスケープ処理そのもの(sb.Replace(...).Replace(...).Rep...)を省いてベンチマークを取ってみました。

つまり string ⇔ UTF-8 変換は必要だがエスケープが不要の MessagePack for C# と同条件(aka. 追い風参考記録)

👇 それがコチラ

.NET 9

書き出しはデータ構造が単純な Twitter JSON はバイナリフォーマットに勝る結果に!

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Save_ToJsonUtf8Cache 1 0.5145 ms 1.00 - NA
Catalog_Save_MsgPack 1 0.5091 ms 0.99 0.33 MB NA
Catalog_Save_ToJsonUtf8Cache 10 5.1211 ms 1.00 - NA
Catalog_Save_MsgPack 10 4.8182 ms 0.94 3.24 MB NA
Twitter_Save_ToJsonUtf8Cache 1 0.3963 ms 1.00 - NA
Twitter_Save_MsgPack 1 0.5031 ms 1.27 0.39 MB NA
Twitter_Save_ToJsonUtf8Cache 10 3.8492 ms 1.00 - NA
Twitter_Save_MsgPack 10 4.8633 ms 1.26 3.88 MB NA

読み込みも同様に Twitter はバイナリフォーマットに勝る結果になりました。

simdjson/yyjson を参考に文字列周りをチューニングすれば、正しい JSON を書き出しつつパフォーマンスを向上させることが出来そうです。

※ SIMD 周りは Untiy との互換性を維持しつつ対応するのが大変ですが、最近のトレンドとして「高速なネイティブ実装のラッパーですべて解決」というのがあるので、そもそも simdjson/yyjson を参考に~~とかじゃなくてラップしちゃえば良かったんじゃね説があります。.jsonc の縛りもなくなるし。

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Load_FromJsonable 1 0.9721 ms 1.00 0.55 MB 1.00
Catalog_Load_MsgPack 1 0.7485 ms 0.77 0.55 MB 1.00
Catalog_Load_FromJsonable 10 11.0955 ms 1.00 5.38 MB 1.00
Catalog_Load_MsgPack 10 9.1816 ms 0.83 5.38 MB 1.00
Twitter_Load_FromJsonable 1 0.7893 ms 1.00 0.49 MB 1.00
Twitter_Load_MsgPack 1 0.9240 ms 1.17 0.49 MB 1.00
Twitter_Load_FromJsonable 10 8.2320 ms 1.00 4.93 MB 1.00
Twitter_Load_MsgPack 10 9.5805 ms 1.16 4.93 MB 1.00

.NET 5(Unity 想定)

Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Save_ToJsonUtf8Cache 1 1.2128 ms 1.00 - NA
Catalog_Save_MsgPack 1 0.7667 ms 0.63 0.33 MB NA
Catalog_Save_ToJsonUtf8Cache 10 12.1620 ms 1.00 - NA
Catalog_Save_MsgPack 10 8.4263 ms 0.69 3.24 MB NA
Twitter_Save_ToJsonUtf8Cache 1 0.6287 ms 1.00 - NA
Twitter_Save_MsgPack 1 0.7143 ms 1.14 0.39 MB NA
Twitter_Save_ToJsonUtf8Cache 10 6.3423 ms 1.00 - NA
Twitter_Save_MsgPack 10 7.3302 ms 1.16 3.88 MB NA
Method Boost Mean Ratio Allocated Alloc Ratio
Catalog_Load_FromJsonable 1 1.5346 ms 1.00 0.55 MB 1.00
Catalog_Load_MsgPack 1 1.3000 ms 0.85 0.55 MB 1.00
Catalog_Load_FromJsonable 10 16.2904 ms 1.00 5.38 MB 1.00
Catalog_Load_MsgPack 10 15.1145 ms 0.93 5.38 MB 1.00
Twitter_Load_FromJsonable 1 1.0804 ms 1.00 0.49 MB 1.00
Twitter_Load_MsgPack 1 0.9662 ms 0.89 0.49 MB 1.00
Twitter_Load_FromJsonable 10 11.6228 ms 1.00 4.93 MB 1.00
Twitter_Load_MsgPack 10 10.1999 ms 0.88 4.93 MB 1.00

ToJsonable メソッド

書き出しは ToJsonUtf8ToJsonToJsonable という3つのメソッドがあります。

ToJson は標準的な JSON を出力しますが、ToJsonable は .jsonc(JSON with Comments)フォーマットで出力します。

これは JSON 最大の弱点であるデータの先読みを不要にするためです。

書き出される JSON は /*JMC1*/(JSON with Metadata Comments v1)というヘッダーで始まりバイトオーダーマークが存在せず、改行や空白も禁止という可読文字のみで構成されたバイナリフォーマットに近いものになっています。

/*JMC1*/{/*#*/"Prop":/*#*/[...],/*#*/"Other":/*#*/{/*#*/"dict":...}}

文字列/辞書/配列の前に要素数をコメントで付与していて、このメタデータによってバイナリフォーマット並みの速度とメモリ効率を実現しています。

(似たような方式で _ をメタデータ用のプロパティーとして付与するモノもあるようです)

各要素の最大長

要素数 ushort0x30(48)から 0x7DFF(32,255)の範囲内という制限があり、最大長は 32,207、Base64 は 24,153 のハードリミットがあります。(ToJson はシステムが許す限り無制限)

これは

/*ランダムなリトルエンディアン2バイト*/

が、予期せず

/**/*/

という UTF-8 文字列を構成することを防ぎ、かつ2バイト目が 0x01 - 0x7E に収まる範囲です。(1バイト目に UTF-8 のリードバイトが来る可能性があるので)

実際は1バイト目は 0x2B 以上、2バイト目は 0x7F(DEL)以下でおkだと思いますが、

/*/**/
/*.{DEL}*/

という本来有効なハズのコメントが適切に処理されない可能性があるので外しています。(2文字目が 0x01 以上なのは VS Code がヌル文字があるとバイナリ扱いするのが面倒だから)

様々な小細工の結果、拡張子を .jsonc にしておけば VS Code で警告なく開けて Alt+Shift+F でフォーマットも可能になっています。

プロパティー名のハッシュ値

プロパティー名の分岐で毎回 UTF-8 バイト列を SequenceEqual するのは微妙なので、プロパティーの分岐は FNV-1a のハッシュ値によって行っています。(いました)

いましたが、どうにも重いので XXHash32 に切り替えてみたりなんだりしたんですが、最終的に「コンパイルが通る時点である程度の分散が保証されている」プロパティー名の特性に合わせて以下の様にハッシュ値を計算しています。

if (len >= 8)
{
    var value = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice((len - 8) >> 1));

    var hash = unchecked((uint)(value >> 32) ^ (uint)value);

    if (len >= 9)
    {
        hash ^= BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(len - 4));
    }

    if (len >= 10)
    {
        hash ^= BinaryPrimitives.ReadUInt32LittleEndian(data);
    }

    return hash;
}
else
{
    return len switch
    {
        >= 4 => BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(len - 4)) ^ ((uint)data[0] << 24),
        >= 1 => data[len - 1],
        _ => 0,
    };
}

かなり割り切っていて、

  • 8文字以上なら中央8文字を使う
    • コレで書き込み速度がバイナリフォーマットを超えた
    • が! 予想以上に衝突する
      • 読み込みで SequenceEqual に流れる確率が上がって 0.1ms ほど遅くなった
    • → 先頭+末尾4文字も加味して衝突確率を下げる
      • 書き込みはちょっと遅くなったが、(結果が良い時は) Twitter のデータ100件の読み込みが MessagePack for C# を超えることも
  • それ以外
    • V0 Value1 PosX みたいにサフィックス以外変わらんだろう
    • 一応、先頭の1文字と末尾の XOR を取るなど

正確さじゃなくて SequenceEqual に流れないようにするのが目的なのでコレで十分ですね。

ちなみに以下の XXHash 実装だと FNV-1a より気持ち遅くなりました。ただ、速度はほぼ変わらずハッシュの品質は爆上がりなので、FNV-1a の出番はバイナリサイズを抑えたい時ぐらいですね。

https://github.com/sator-imaging/Unity-Fundamentals/blob/main/Runtime/Hashing/MiniXXHash.cs

加えて switch でジャンプテーブルが生成されるように、プロパティーの数が多い場合はハッシュを 0-n にリマップしています。

👇 Twitter JSON(ステータス)のプロパティー名のハッシュ値一覧

衝突無し!

呼び出し側の負担を減らす

MethodImplOptions.AggressiveInlining と同様に暗黙的な型変換も「呼び出し側負担」になります。

int Test(ReadOnlySpan<byte> span)  // スパンで受ける!
{
    return span.Length;
}

// Code size 33 (0x21)
void X()
{
    var x = new byte[]{ 1, 2, 3 };
    Console.WriteLine(Test(x));     // implicit operator ReadOnlySpan<T>(T[]) が呼ばれる
}

👇

int Test(byte[] span)  // あえて byte[] で受ける!!
{
    return span.Length;
}

// Code size 28 (0x1c)
void X()
{
    var x = new byte[] { 1, 2, 3 };
    Console.WriteLine(Test(x));
}

ReadOnlySpan<byte> を使うのは適切ですが、呼び出し側が byte[] として扱っている場合メソッドサイズが呼び出し毎に5バイト増えることになります。なので、ホットパスで使われるヘルパー関数はあえて byte[] で受けて、ヘルパー関数内でスパンに変換するという選択肢も十分に検討する価値があります。

Jsonable ではプロパティー名の書き込みヘルパー関数は byte[] で受けるので、全ての ToJsonable メソッドは5バイト x プロパティー数のサイズ削減が期待できます。

(おそらくベンチマーク結果には何の影響も与えないです)

その他

その他にも、

  • ループ内でのクラスフィールドやプロパティーへのアクセスを排除
  • 基本失敗しない IBufferWriter<T> への書き込みのエラーチェックを最後にまとめることでコードサイズを削減
  • Unity のために where TWriter : IBufferWriter<T> する
  • (意味があるか不明)書き出し時にプロパティーを型で並べ替えることで同じヘルパー関数の呼び出しを固める

等の基本的な措置も行っています。

データの部分更新とインスタンスの再利用

JSON の魅力の一つは GraphQL の様に全体ではなく必要な部分だけ更新できることです。

ToJson はオプションで部分書き出しに対応できるようになっています。

[ToJson(Property = nameof(Position))]
[ToJson, FromJson]
partial record Composite
{
    public float[]? Position { get; set; }
    public float[]? Rotation { get; set; }
    public float[]? Scale { get; set; }
    public Payload? Payload { get; set; }
}

[ToJson, FromJson] partial record Payload(...) { }

// 書き出し用の型を定義しても良い
[ToJson] partial record struct PayloadOnly(Payload Payload) { }

👇

var comp = new Composite();
var writer = new ArrayBufferWriter<byte>();

comp.ToJsonUtf8_Position(writer, emitMetadataComments: true);  // true がデフォ
comp.FromJsonable(writer.WrittenMemory);

Console.WriteLine(comp);     // Position だけ埋まってる
writer.ResetWrittenCount();  // Unity の場合はメモリのクリアを伴う .Clear を使うしかない

// record struct なので new してもアロケ無し(書き出すデータそのものは除く)
new PayloadOnly(new(...)).ToJsonable(writer);
comp.FromJsonable(writer.WrittenMemory);

インスタンスの再利用

配列やコレクション、Jsonable 対応型のインスタンスを再利用することができます。

data.FromJson(utf8, reuseInstance: true);

再利用方式

  • T[] Base64: 要素数が一致した場合のみ再利用(書き換え)
  • ICollection<T>: 再利用しない
  • List<T>: 常に要素を追加
  • Dictionary IDictionary: キーが存在すれば更新、存在しなければ追加

リストや辞書の扱いを変えたい場合は OnWillDeserialize を実装します。

[FromJson]
partial class Foo
{
    public List<int> Values { get; set; }

    partial void OnWillDeserialize()
    {
        Values.Clear();  // 読み込み前に毎回中身をクリアする
    }
}

パフォーマンスへの影響

ArrayBufferWriter のキャッシュで書き出しが高速化するベンチマークから分かるように、配列の確保はそこそこのコストがかかります。

.NET 9 のベンチは GC.AllocateUninitializedArray<T> を使っているので初期化を行うかどうかはあまり関係無い模様。

なので、インスタンスの再利用を行うことで処理速度がかなり向上します。配列の要素数が多いと特に顕著です。

※ リストでアロケが減らないのは、常に statusList.Add(new Status()) するからです。

| Method                              | Boost | Mean       | Ratio | Allocated | Alloc Ratio |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Catalog_Load_FromJsonable           | 1     |  0.9457 ms |  1.00 |   0.55 MB |        1.00 |
| Catalog_Load_MsgPack                | 1     |  0.7654 ms |  0.81 |   0.55 MB |        1.00 |
| Catalog_Load_FromJsonableReuseArray | 1     |  0.8810 ms |  0.93 |   0.09 MB |        0.16 |
| Catalog_Load_FromJsonableReuseList  | 1     |  0.9192 ms |  0.97 |   0.54 MB |        0.98 |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Twitter_Load_FromJsonable           | 1     |  0.9166 ms |  1.00 |   0.55 MB |        1.00 |
| Twitter_Load_MsgPack                | 1     |  0.8882 ms |  0.97 |   0.49 MB |        0.90 |
| Twitter_Load_FromJsonableArray      | 1     |  0.9376 ms |  1.02 |   0.43 MB |        0.79 |
| Twitter_Load_FromJsonableReuseList  | 1     |  0.8635 ms |  0.94 |   0.55 MB |        1.00 |

配列とリストでアロケーションに差はありますが、処理速度がほぼ変わらないのは興味深いです。1,000 件のリストを確保するという処理が単純に遅いという事が良く分かります。

capacity を指定しているので内部バッファーの拡張がベンチマークに含まれることはない)

👇 配列サイズ10倍

| Method                              | Boost | Mean       | Ratio | Allocated | Alloc Ratio |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Catalog_Load_FromJsonable           | 10    | 14.0057 ms |  1.00 |   5.38 MB |        1.00 |
| Catalog_Load_MsgPack                | 10    |  9.1853 ms |  0.66 |   5.38 MB |        1.00 |
| Catalog_Load_FromJsonableReuseArray | 10    |  9.4582 ms |  0.68 |    0.8 MB |        0.15 |
| Catalog_Load_FromJsonableReuseList  | 10    | 10.5262 ms |  0.75 |   5.31 MB |        0.99 |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Twitter_Load_FromJsonable           | 10    | 13.3592 ms |  1.00 |   5.49 MB |        1.00 |
| Twitter_Load_MsgPack                | 10    | 10.0158 ms |  0.75 |   4.93 MB |        0.90 |
| Twitter_Load_FromJsonableArray      | 10    |  8.9105 ms |  0.67 |   4.34 MB |        0.79 |
| Twitter_Load_FromJsonableReuseList  | 10    |  9.6351 ms |  0.72 |   5.48 MB |        1.00 |

Unity 対応とファイルの互換性

Unity は 2022 以降で使えます。Unity 2021 でも動くハズ、というか Unity が生成した .csproj 的には動いてるんですが、生成された ToJson FromJson アトリビュートが「Unity のコンパイラーから見えない」という問題があって使えません。残念。

(ビルドしたアセンブリは Unity 2021 でも動く)

データの互換性確保については、確認のための Jsonable.Assertions という追加パッケージがあります。

アプリの初期化時など、.jsonc の初回読み込みの後に呼ぶ感じです。コレによって何かあっても Json.NET に乗り換えられるという安心感を得ることが出来ます。

// 書き出した .jsonc が読み戻せるかと、Json.NET を使った互換性チェックを行う(DEBUG ビルドのみ)
JsonableDebugger.AssertRoundtrip(instance, skipJsonDotNetTests: false);

// 日付やランダムな ID など比較の妨げになるものは適宜除外
JsonableDebugger.AssertRoundtrip(instance, false, new string[] { "RandomId", "DeserializedAt" });

// 全てのプロパティーが完全に一致するかを System 名前空間の構造体が見つかるまで再帰的に調べるメソッド
// JSON に書き出すかどうかに関わらず、public も private も根こそぎチェック(roundtrip 内でも実行される)
JsonableDebugger.AssertPropertiesEqual(expected, actual);

※ Json.NET は Unity 公式の UPM パッケージが用意されています。

https://docs.unity3d.com/Packages/com.unity.nuget.newtonsoft-json@3.2/manual/index.html

特殊な浮動小数点

NaN PositiveInfinity NegativeInfinity には float double 共に対応していません。(パースエラーになります)

JSON の仕様(RFC 8259)的にも禁止事項なのでおkでしょう。シリアライザー側で値の制限は行っていないので、書き出し前または OnWillSerialize で弾く必要があります。

MessagePack フォーマットについて

MessagePack for C# のベンチマークでは MessagePackObject(keyAsPropertyName: true) を使っています。また、.NET 10 だと動かなかったので .NET 9 で実行しています。

何故ベンチマークでプロパティー名をキーにしているかと言うと、MessagePack というフォーマットが型の定義しか持たないためです。

JSON はスキーマによって型や値の範囲を指定できますが、スキーマ無しでも「文字列キーと値のペア」というデータ構造が保証されています。

しかし MessagePack は値の型しか規約が無いので、シリアライザーと組み合わせて初めてバイナリデータに意味が出るという結構ピーキーなフォーマットになっています。

map32 という型が定義されているのでそれを使えば良いように思えますが、「map32 をオブジェクトとして扱う」かどうかはシリアライザー次第です。場合によっては map は辞書限定で、オブジェクトは ext32 で型情報が付与されていないとデシリアイズしないということもあり得ます。別のシリアライザーは ext32 を一切読み込まないという可能性もあります。

そしてなにより、データ構造がシリアライザーによって決まるので

  • 使うシリアライザーを決定した時点でデータ構造が確定し型のメタデータを見る意味が無い

って感じなのもちょっとアレです。送信元が信頼できない可能性もあるので型チェックをしたとして、しかしそれは何かの保証にはならず、信頼して決め打ちで読み込むのならそもそも構造体のメモリ表現を直接送っちゃう方式で良くない? でもあります。

最低限頭に MessagePack であることを示すヘッダーとバージョン(MessagePack ではなくデータ構造のバージョン)を付与する仕様はあった方が良かったんじゃないでしょうか。良く分からないデータの構造を仮定してとりあえず読み込んでみて、構造がマッチしなかったら破棄! って感じでなんか怖いですね。

長くなりましたが、以上の理由により実際の MessagePack の使い方としては

  • キーと値のペア
  • 独自のヘッダーを付与して MessagePack のバイナリを包む
    • MemoryMarshal.Read/Write<T> が使用可能ならそっちの方が良い

どちらかしか無いだろうって所からです。

System.Text.Json と UTF-8

UTF-8 のダイレクト読み書きを行うと読み込みが遅くなって書き出しは速くなるようです。(使い方間違ってるかも?)

| Method                              | Boost | Mean       | Ratio | Allocated | Alloc Ratio |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Catalog_Save_ToJsonUtf8Cache        | 1     |  0.6114 ms |  1.00 |      0 MB |        1.00 |
| Catalog_Save_SysTxtJson             | 1     |  0.6918 ms |  1.13 |   0.96 MB |    2,610.18 |
| Catalog_Save_SysTxtJsonUtf8         | 1     |  0.5419 ms |  0.89 |         - |        0.00 |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Twitter_Save_ToJsonUtf8Cache        | 1     |  0.5457 ms |  1.00 |   0.28 MB |        1.00 |
| Twitter_Save_SysTxtJson             | 1     |  0.8105 ms |  1.49 |   1.11 MB |        4.00 |
| Twitter_Save_SysTxtJsonUtf8         | 1     |  0.6099 ms |  1.12 |         - |        0.00 |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Catalog_Save_ToJsonUtf8Cache        | 10    |  5.8673 ms |  1.00 |      0 MB |        1.00 |
| Catalog_Save_SysTxtJson             | 10    |  7.6414 ms |  1.30 |   9.49 MB |    2,590.00 |
| Catalog_Save_SysTxtJsonUtf8         | 10    |  5.1799 ms |  0.88 |         - |        0.00 |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Twitter_Save_ToJsonUtf8Cache        | 10    |  5.5676 ms |  1.00 |   2.77 MB |        1.00 |
| Twitter_Save_SysTxtJson             | 10    |  8.8821 ms |  1.60 |  11.06 MB |        4.00 |
| Twitter_Save_SysTxtJsonUtf8         | 10    |  6.0311 ms |  1.08 |         - |        0.00 |
| Method                              | Boost | Mean       | Ratio | Allocated | Alloc Ratio |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Catalog_Load_FromJsonable           | 1     |  0.9457 ms |  1.00 |   0.55 MB |        1.00 |
| Catalog_Load_SysTxtJson             | 1     |  4.9116 ms |  5.19 |   1.15 MB |        2.08 |
| Catalog_Load_SysTxtJsonUtf8         | 1     |  7.4355 ms |  7.86 |   1.15 MB |        2.08 |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Twitter_Load_FromJsonable           | 1     |  0.9166 ms |  1.00 |   0.55 MB |        1.00 |
| Twitter_Load_SysTxtJson             | 1     |  1.8318 ms |  2.00 |   0.54 MB |        0.99 |
| Twitter_Load_SysTxtJsonUtf8         | 1     |  2.4017 ms |  2.62 |   0.54 MB |        0.99 |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Catalog_Load_FromJsonable           | 10    | 14.0057 ms |  1.00 |   5.38 MB |        1.00 |
| Catalog_Load_SysTxtJson             | 10    | 36.9992 ms |  2.64 |  11.31 MB |        2.10 |
| Catalog_Load_SysTxtJsonUtf8         | 10    | 45.4580 ms |  3.25 |  11.31 MB |        2.10 |
|------------------------------------ |------ |-----------:|------:|----------:|------------:|
| Twitter_Load_FromJsonable           | 10    | 13.3592 ms |  1.00 |   5.49 MB |        1.00 |
| Twitter_Load_SysTxtJson             | 10    | 16.3961 ms |  1.23 |    5.4 MB |        0.98 |
| Twitter_Load_SysTxtJsonUtf8         | 10    | 19.3463 ms |  1.45 |    5.4 MB |        0.98 |

埋め込み型の利点/欠点

埋め込み型は外部ライブラリに頼らず自己完結しているので、

partial void OnWillSerialize();
partial void OnDidSerialize();

partial void OnWillDeserialize();
partial void OnDidDeserialize();

これらを実装してやれば、バリデーション含め単体で完全に機能するアセンブリとしてビルドできます。

ただ、処理がアセンブリに埋め込まれているので「外部ライブラリが更新されて何もしていないのに速くなった!」が無いのが欠点です。

埋め込まれるヘルパー関数群はめちゃくちゃ薄く作ってあるので、各アセンブリ毎に複製しても問題にはならないでしょう。

TODO

Memory<T>

Memory<T> のエンコード/デコードにもせっかくなら対応したい。ただ、使っている Roslyn のバージョンでは SpecialTypeSystem_Memory_T は定義されていない。ちょっと面倒。

private のセッター/ゲッターに対応したほうが幅が広がって良い可能性あるが。

ルート配列

もちろん対応していないので、良くあるハック

{ "RootArray": <元のJSON文字列> }

で対応します。

.jsonl(JSON Lines) や .ndjson(Newline Delimited JSON)も同様に上手いことやって読み込みます。

文字列処理

エスケープが必要かの確認は 0x2F 以下の文字コードか、または " \ であるかなので SIMD 化も楽だが、エスケープ/アンエスケープ処理は文字数が変わるという問題があって、色々調べても「普通に頑張る」しか出てこない。何か決定版と言えるアルゴリズムがありそうなんですけどねー。

8とか16バイト単位でエスケープ対象文字の存在を調べて、含まれていたらエスケープを行うパス、含まれていなければ単純コピーするパスに流すぐらいしかでき無さそうな?

おわりに

最初は軽い気持ちで AI にコード生成させたんですが、「せっかくだから」で無駄にこだわって独自の拡張フォーマットまで作ってしまいました。いまさら JSON の読み書きを高速化してどうする感と、そもそもテストデータが複雑すぎてベンチマークが何の参考にもならないのも問題ですね。

無理矢理意味を見出すとすれば、そこそこ複雑な Twitter のステータス 100 件の読み書きが 4ms で終わるので、60fps 換算で残り 12ms 分の追加処理が可能。雑に 50 体程度のプレイヤーの状態の同期がネットワーク越しに毎フレーム実行出来るという事になります。読み書き共に別スレッドに逃がせるので実際はもうちょっと余裕がありそう?

Unity でも辞書をシリアライズ出来て、ネットワーク越しにデータを同期する場合もアロケを回避しつつ高速化、デバッガビリティーも高いのでそこそこ良い選択肢なんじゃないでしょうか。読み込みは .jsonc 限定という制約がありますが、何かトラブっても Json.NET か System.Text.Json に切り替えが可能、一見雑なフォーマットに見えて抑えるべきは抑えてる、新興言語でも絶対に使える JSON の安心感は代えがたいものがあります。

https://www.nuget.org/packages/SatorImaging.Jsonable

以上です。お疲れ様でした。

脚注
  1. https://github.com/simdjson/simdjson/tree/master/jsonexamples ↩︎ ↩︎ ↩︎

Discussion