Utf8Json
ライブラリ
Utf8Json - C#向け最速・ゼロアロケーションJSONシリアライザー
概要
Utf8Jsonは、C#(.NET、.NET Core、Unity、Xamarin)向けに開発された超高速JSONシリアライザーです。UTF-8バイナリを直接読み書きすることで、文字列変換のオーバーヘッドを完全に排除し、ゼロアロケーションを実現しています。動的なILコード生成、オートマタベースのプロパティマッチング、独自の数値変換アルゴリズムなど、徹底的なパフォーマンス最適化により、既存のJSONライブラリを大幅に上回る性能を達成しています。
詳細
Utf8Jsonは、MessagePackSharpやZeroFormatterの作者であるneuecc(Yoshifumi Kawai)氏が開発した、パフォーマンスに特化したJSONシリアライザーです。従来のJSONライブラリがUTF-16文字列への変換を必要とするのに対し、Utf8JsonはUTF-8バイト配列を直接処理することで、劇的なパフォーマンス向上を実現しています。
ライブラリの核心的な特徴は、ゼロアロケーション設計です。64KB以下のバッファはスレッドローカルメモリプールから取得され、ガベージコレクションの負荷を最小限に抑えます。また、実行時に動的にILコードを生成してカスタム型用の最適化されたフォーマッターを作成し、AOT環境向けには事前コード生成もサポートしています。
プロパティマッチングにはオートマタベースのアプローチを採用し、生のUTF-8バイトから直接プロパティ名を照合します。数値変換では独自のitoa/atoi、dtoa/atodアルゴリズムを実装し、文字列を経由しない直接変換を実現。これらの最適化により、既存のJSONライブラリと比較して2〜4倍の性能向上を達成しています。
注意点として、オリジナルのリポジトリはアーカイブされており、現在はコミュニティフォークの使用が推奨されています。
メリット・デメリット
メリット
- 圧倒的な高速性: 既存のJSONライブラリの2〜4倍のパフォーマンス
- ゼロアロケーション: メモリプール使用でGC負荷を最小化
- 直接UTF-8処理: 文字列変換のオーバーヘッドを完全に排除
- Unity対応: Vector3、Quaternionなど Unity固有型をネイティブサポート
- 柔軟なカスタマイズ: Resolver/Formatterシステムで高度な制御が可能
- AOT環境対応: 事前コード生成によりXamarin、Unity IL2CPPで動作
デメリット
- メンテナンス停止: オリジナルリポジトリはアーカイブ状態
- 複雑性: 高度な最適化により内部実装が複雑
- 機能制限: パフォーマンス優先のため一部高度な機能(循環参照等)未対応
- 学習曲線: Resolver/Formatterシステムの理解に時間が必要
- デバッグ困難: 動的生成されたILコードのデバッグが困難
- 標準化の欠如: System.Text.Jsonの登場により標準ライブラリとの差が生じる
参考ページ
- Utf8Json公式リポジトリ(アーカイブ済み)
- ZCS.Utf8Json - コミュニティフォーク
- Utf8Json Performance Benchmarks
- The Battle of C# JSON Serializers
書き方の例
1. 基本的なシリアライゼーション
using Utf8Json;
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime BirthDate { get; set; }
}
class BasicExample
{
static void Main()
{
var person = new Person
{
Name = "田中太郎",
Age = 30,
BirthDate = new DateTime(1993, 5, 15)
};
// オブジェクト → byte[](UTF-8)
byte[] utf8Json = JsonSerializer.Serialize(person);
// byte[] → オブジェクト
var deserialized = JsonSerializer.Deserialize<Person>(utf8Json);
// オブジェクト → 文字列
string jsonString = JsonSerializer.ToJsonString(person);
Console.WriteLine(jsonString);
// ストリームへの書き込み
using (var stream = new FileStream("person.json", FileMode.Create))
{
JsonSerializer.Serialize(stream, person);
}
}
}
2. プライベートメンバーのシリアライゼーション
public class Employee
{
private string employeeId = "EMP-001";
private double salary = 50000;
public string Name { get; set; }
public string Department { get; set; }
// プライベートフィールドへのアクセサー
public string GetEmployeeId() => employeeId;
public double GetSalary() => salary;
}
class PrivateMemberExample
{
static void SerializePrivateMembers()
{
var employee = new Employee
{
Name = "山田花子",
Department = "営業部"
};
// プライベートメンバーを含めてシリアライズ
var json = JsonSerializer.ToJsonString(employee, StandardResolver.AllowPrivate);
Console.WriteLine(json);
// 出力: {"employeeId":"EMP-001","salary":50000,"Name":"山田花子","Department":"営業部"}
// プライベートメンバーも復元してデシリアライズ
var deserialized = JsonSerializer.Deserialize<Employee>(json, StandardResolver.AllowPrivate);
Console.WriteLine($"ID: {deserialized.GetEmployeeId()}, 給与: {deserialized.GetSalary()}");
}
}
3. カスタムリゾルバーとフォーマッター
using Utf8Json.Formatters;
using Utf8Json.Resolvers;
// カスタムフォーマッター
public class DateOnlyFormatter : IJsonFormatter<DateTime>
{
public void Serialize(ref JsonWriter writer, DateTime value, IJsonFormatterResolver formatterResolver)
{
writer.WriteString(value.ToString("yyyy-MM-dd"));
}
public DateTime Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
{
var dateString = reader.ReadString();
return DateTime.ParseExact(dateString, "yyyy-MM-dd", null);
}
}
// カスタムリゾルバー
public class CustomResolver : IJsonFormatterResolver
{
public static IJsonFormatterResolver Instance = new CustomResolver();
private CustomResolver() { }
public IJsonFormatter<T> GetFormatter<T>()
{
if (typeof(T) == typeof(DateTime))
{
return (IJsonFormatter<T>)new DateOnlyFormatter();
}
return StandardResolver.Default.GetFormatter<T>();
}
}
// 使用例
class CustomFormatterExample
{
static void UseCustomFormatter()
{
var data = new { Date = DateTime.Now, Name = "テスト" };
var json = JsonSerializer.ToJsonString(data, CustomResolver.Instance);
Console.WriteLine(json); // {"Date":"2023-12-15","Name":"テスト"}
}
}
4. Unity向けの使用例
using UnityEngine;
using Utf8Json;
using Utf8Json.Resolvers;
[System.Serializable]
public class GameData
{
public string PlayerName { get; set; }
public int Score { get; set; }
public Vector3 Position { get; set; }
public Quaternion Rotation { get; set; }
public Color PlayerColor { get; set; }
}
public class UnityExample : MonoBehaviour
{
void Start()
{
// Unityリゾルバーをデフォルトに設定
JsonSerializer.SetDefaultResolver(StandardResolver.Default);
var gameData = new GameData
{
PlayerName = "Player1",
Score = 1000,
Position = new Vector3(10f, 5f, 3f),
Rotation = Quaternion.Euler(0, 45, 0),
PlayerColor = Color.red
};
// Unity型を含むオブジェクトのシリアライズ
byte[] jsonBytes = JsonSerializer.Serialize(gameData);
// PlayerPrefsに保存
string base64 = System.Convert.ToBase64String(jsonBytes);
PlayerPrefs.SetString("GameData", base64);
// 読み込み
string loadedBase64 = PlayerPrefs.GetString("GameData");
byte[] loadedBytes = System.Convert.FromBase64String(loadedBase64);
var loadedData = JsonSerializer.Deserialize<GameData>(loadedBytes);
Debug.Log($"Loaded: {loadedData.PlayerName}, Score: {loadedData.Score}");
}
}
5. 動的JSON処理
class DynamicJsonExample
{
static void ProcessDynamicJson()
{
// 動的JSONのデシリアライズ
var json = @"{
""name"": ""商品A"",
""price"": 1500,
""tags"": [""新商品"", ""おすすめ""],
""details"": {
""weight"": 500,
""dimensions"": {
""width"": 10,
""height"": 20,
""depth"": 5
}
}
}";
dynamic obj = JsonSerializer.Deserialize<dynamic>(json);
// 動的アクセス
string name = obj["name"]; // "商品A"
double price = obj["price"]; // 1500
var firstTag = obj["tags"][0]; // "新商品"
var weight = obj["details"]["weight"]; // 500
var width = obj["details"]["dimensions"]["width"]; // 10
Console.WriteLine($"商品: {name}, 価格: {price}円");
// 動的オブジェクトの構築
var newObj = new Dictionary<string, object>
{
["id"] = 123,
["items"] = new[] { "item1", "item2" },
["metadata"] = new Dictionary<string, object>
{
["created"] = DateTime.Now,
["version"] = 1.0
}
};
string newJson = JsonSerializer.ToJsonString(newObj);
Console.WriteLine(newJson);
}
}
6. 高度なパフォーマンス最適化
using Utf8Json;
using System.Buffers;
class PerformanceOptimization
{
// ArraySegmentを使用したゼロコピー処理
static void ZeroCopyProcessing()
{
var largeObject = GenerateLargeObject();
// ArraySegmentで内部バッファプールを使用
ArraySegment<byte> segment = JsonSerializer.SerializeUnsafe(largeObject);
try
{
// セグメントを直接処理(コピーなし)
ProcessJsonBytes(segment.Array, segment.Offset, segment.Count);
}
finally
{
// 重要: ArraySegmentは使用後すぐに返却
JsonSerializer.ReturnArrayToPool(segment.Array);
}
}
// ストリーミング処理
static async Task StreamingSerializationAsync()
{
var items = GetLargeDataset(); // 大量のデータ
using (var stream = new FileStream("large.json", FileMode.Create))
using (var writer = new StreamWriter(stream))
{
await writer.WriteAsync("[");
bool first = true;
foreach (var item in items)
{
if (!first) await writer.WriteAsync(",");
first = false;
// 各アイテムを個別にシリアライズ
var json = JsonSerializer.ToJsonString(item);
await writer.WriteAsync(json);
}
await writer.WriteAsync("]");
}
}
// 事前生成されたフォーマッターの使用(AOT環境向け)
static void UsePreGeneratedFormatter()
{
// AOT環境では事前生成が必要
// Utf8Json.UniversalCodeGenerator.exe を使用して生成
Utf8Json.Resolvers.GeneratedResolver.Instance.Register();
var obj = new MyGeneratedType { Value = 42 };
byte[] json = JsonSerializer.Serialize(obj);
}
}