埋め込み型 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 行超
です。
ToJsonUtf8Cache
は ArrayBufferWriter<byte>
インスタンスを使いまわしているので、見えていないだけで1回分のアロケーションはあるハズです。
キャッシュ無しの ToJsonUtf8 との差を見ると配列の確保は割と時間がかかる処理なのが良く分かります。
[1]
Citm_CatalogMethod | 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 |
[1:1]
TwitterMethod | 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 |
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 |
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 |
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 |
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 |
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"
},
...
],
...
}
重いのは文字列処理
文字列処理が重しになっているのは明らかなので、試しに EscapeString
と UnescapeString
からエスケープが必要かの判定(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
メソッド
書き出しは ToJsonUtf8
、ToJson
と ToJsonable
という3つのメソッドがあります。
ToJson は標準的な JSON を出力しますが、ToJsonable は .jsonc
(JSON with Comments)フォーマットで出力します。
これは JSON 最大の弱点であるデータの先読みを不要にするためです。
書き出される JSON は /*JMC1*/
(JSON with Metadata Comments v1)というヘッダーで始まりバイトオーダーマークが存在せず、改行や空白も禁止という可読文字のみで構成されたバイナリフォーマットに近いものになっています。
/*JMC1*/{/*#*/"Prop":/*#*/[...],/*#*/"Other":/*#*/{/*#*/"dict":...}}
文字列/辞書/配列の前に要素数をコメントで付与していて、このメタデータによってバイナリフォーマット並みの速度とメモリ効率を実現しています。
(似たような方式で _
をメタデータ用のプロパティーとして付与するモノもあるようです)
各要素の最大長
要素数 ushort
は 0x30
(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 パッケージが用意されています。
特殊な浮動小数点
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 のバージョンでは SpecialType
に System_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 の安心感は代えがたいものがあります。
以上です。お疲れ様でした。
Discussion