linq.jsやZeroFormatterなどステッキーなライブラリを作っているneuecc先生の新作JSONシリアライザーのUtf8Jsonを試してみました。
Utf8Json - Fast JSON Serializer for C#
https://github.com/neuecc/Utf8Json
”Definitely Fastest”(明らかに最速)とのことなので、最速の恩恵を受けそうな例で実践します。拙作で恐縮ですが、C#で仮想通貨取引所のリアルタイムレートを記録するプログラムを作ってみたを1週間実行したところ、合計1GBを軽く超えるJSONファイルが生成できました。それをデシリアライズするプログラムを作ります。そして、既存のJSONシリアライザーとの比較を行います。
環境:VisualStudio2017 Community / .NET 4.6.0
ライブラリのバージョン
- DataContractJsonSerializer : System.Runtime.Serialization 4.0.0.0
- Json.NET : v10.0.3 (2017/6/8)
- ServiceStack.Text : v5.0.2 (2018/1/2)
- Jil : v2.15.4 (2017/8/24)
- DynamicJson : v1.2.0 (2011/9/22)
- Utf8Json : v1.3.6.1 (2018/1/23)
関連記事:
.NETのJSONシリアライザの速度比較
https://qiita.com/t_takahari/items/6855dfe78071bb5eaef6
##対象データ
仮想通貨取引所のストリーミングAPIから配信される、ビットコイン円(btc-jpy)の取引(板情報+トランザクションデータ)。おおよそ1秒に1回~2回以下のようなJSONが送られてきます。JSON Viewerで見ると、次のような構造であることがわかります。
1つのJSONを1つのテキストファイルとして保存します。1つのテキストファイルはおよそ4KBです。
last_price.priceとtimestampを抽出するプログラムを作ります。これらのテキストファイルは一定時間(設定では1分、5分)で1つのZipアーカイブに保存されています。途中でこの設定値変えてしまったので1日あたりのアーカイブ数はいい加減です。1/18~1/23の6日分のデータ用意します。エクスプローラーで見ると次のようになります。
35.5万個のJSONが無圧縮で1.43GB(ディスク上は2.70GB)です。32bitプロセスのメモリ制限があるので、まずこれをList<string>としてすべてメモリに落とし込んでみます
var txtList = new List<string>();
var zips = Enumerable.Range(18, 6).Select(x => "btc_jpy\\2018\\201801" + x)
.SelectMany(x => Directory.GetFileSystemEntries(x, "*.zip", SearchOption.AllDirectories));
var sw = new Stopwatch();
sw.Start();
foreach(var z in zips)
{
using (var archive = ZipFile.OpenRead(z))
{
foreach (var entry in archive.Entries)
{
using (var sr = new StreamReader(entry.Open()))
{
txtList.Add(sr.ReadToEnd());
}
}
}
}
sw.Stop();
Console.WriteLine("Zip load : " + sw.Elapsed);
Console.WriteLine("Memory : " + Environment.WorkingSet.ToString("N0") + "bytes");
展開後のtxtファイルではなく、展開前のZipから直接読ませていますが、こっちのほうが速いからです(後述)。実行結果は次のようになります。
Zip load : 00:00:49.7493200
Memory : 3,179,085,824bytes
メモリ使用量は3.0GBです。6日ではなく、7日分読み込ませると「System.OutOfMemoryException」が出ます。なので、これが32bitプロセスの限界とみなしてもよいと思います。64bitでビルドすればほぼ際限なくいけますが、JSONデシリアライズのパフォーマンスを図るには32bitのMAXで十分でしょう。たかだか1~2GBでビッグデータと言い張る気は毛頭ないですが、ビッグデータに片足の親指の爪先ぐらい突っ込んだぐらいのサイズはあります。
また、一気にtxtListに読み込ませるのではなく、Zipアーカイブ単位で読み込ませ、その都度デシリアライズしていくと(実践的にはこちらを使います)、メモリ効率はいいですが、Zipの展開やファイルの読み込みにかかる処理時間と、JSONをパースするのにかかる処理時間を切り離しできなくなるので、ここでは一気にメモリを読ませる方法を使います。
txtListから読み込ませても、正確には、txtListから文字列を読み込ませる処理時間を切り離していないのですが、同じメモリ内での操作なので無視できるぐらい小さいとします。
Zip→txtListではなく、Zip→Txt→txtListと読み込ませると、Zip→Txtの処理を事前に行っておいてもZip→txtListの場合より遅くなります。試してみます。decompressフォルダにはbtc_jpyフォルダ以下のZipアーカイブがそのままの形で展開されているものとします。
var txtList = new List<string>();
var sw = new Stopwatch();
var dirs = Enumerable.Range(18, 6).Select(x => "decompress\\2018\\201801" + x);
sw.Start();
foreach (var d in dirs)
{
foreach(var txt in Directory.GetFileSystemEntries(d, "*.txt", SearchOption.AllDirectories))
{
using(var fs = new StreamReader(txt))
txtList.Add(fs.ReadToEnd());
}
}
sw.Stop();
Console.WriteLine("Text load : " + sw.Elapsed);
Console.WriteLine("Memory : " + Environment.WorkingSet.ToString("N0") + "bytes");
50秒対26分で、明らかに遅いです。CPUパワーは全然使わないんですけどね。ファイルのオーバーヘッドぱない。
Text load : 00:26:46.2992704
Memory : 3,190,595,584bytes
とりあえずZipを使いましたが、この手の使い方をする場合、圧縮はパワー要しても解凍が軽いタイプの圧縮アルゴリズムを選ぶとよさそうな気がします。
##JSONシリアライザー一覧
以下のJSONシリアライザーを比較します。
- DataContractJsonSerializer(.NET標準)
- Json.NET(MIT)
- ServiceStack.Text(AGPLv3 FOSS License Exception)
- Jil(MIT)
- DynamicJSON(Utf8Jsonと作者同じ / Ms-PL)
- Utf8Json(New!! / MIT)
1番目以外はdynamic型が使えるので、型を指定する場合、型を指定せずdynamicでデシリアライズする場合をそれぞれを見ます。
dynamicの場合、型を指定する場合と同様の返し方をすると少し不公平なので(キャストが多い分当然遅くなる)、必要なtimestampとlast_price.priceのみに絞ってTupleで返すようにします。今回の例では、dynamicでデシリアライズする変数の数が、型を指定する場合とそこまで変わらないので、基本的に型を指定したほうが速いです。ただし、文字と数字が混在した配列など不定形なJSONをデシリアライズする場合は、dynamicを使ったほうが都合がいいことがあるので、必要に応じて使い分けるのがいいかと思います。
型を指定する場合のクラスをDataとして次のようにします。
public class Data
{
public LastPrice last_price { get; set; }
public string timestamp { get; set; }
public class LastPrice
{
public string action { get; set; }
public double price { get; set; }
}
}
ライブラリの数が多いので、ライブラリごとに型を指定する場合、dynamicの場合とラッパーメソッドを作ります(普段はこんなことしなくていいです)。
###1.DataContractJsonSerializer
typeofで指定してStreamからしか読めないというのはちょっとなーという感
using System.IO;
using System.Runtime.Serialization.Json;
public static class DataContractJsonSerialize
{
public static Data Deserialize(string jsonStr)
{
var serializer = new DataContractJsonSerializer(typeof(Data));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(jsonStr)))
{
return (Data)serializer.ReadObject(ms);
}
}
}
###2.Json.NET
一番有名なライブラリ。dynamicの場合が面白い書き方
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public static class JsonNet
{
public static Data Deserialize(string jsonStr)
{
return JsonConvert.DeserializeObject<Data>(jsonStr);
}
public static (string, double) DeserializeDynamic(string jsonStr)
{
dynamic dyn = JObject.Parse(jsonStr);//Newtonsoft.Json.Linq
return ((string)dyn.timestamp, (double)dyn.last_price.price);
}
}
###3.ServiceStack.Text
dynamicの場合、内部で文字列になるのが少しトリッキー。(double)とかキャストすると例外になる。
using ServiceStack;
using ServiceStack.Text;
public static class ServiceStackJson
{
public static Data Deserialize(string jsonStr)
{
return JsonSerializer.DeserializeFromString<Data>(jsonStr);//ServiceStack.Text.JsonSerializer
}
public static (string, string) DeserializeDynamic(string jsonStr)
{
var dyn = DynamicJson.Deserialize(jsonStr);//ServiceStack.DynamicJson
return ((string)dyn.timestamp, (string)dyn.last_price.price);//小数でも文字列として記録される
}
}
###4.Jil
直感的でわかりやすい
using Jil;
public static class JilJson
{
public static Data Deserialize(string jsonStr)
{
return JSON.Deserialize<Data>(jsonStr);
}
public static (string, double) DeserializeDynamic(string jsonStr)
{
var dyn = JSON.DeserializeDynamic(jsonStr);
return ((string)dyn.timestamp, (double)dyn.last_price.price);
}
}
###5.DynamicJSON
"Dynamic"JSONなので、型指定の場合はdynamicの変数をキャストしてデシリアライズするというピーキーな仕様。プロジェクトにDynamicJSON.csが追加されるのが特徴。個人的には昔だいぶお世話になりました。
using Codeplex.Data;
public static class DynamicJsonDeserialize
{
public static Data Deserialize(string jsonStr)
{
return (Data)DynamicJson.Parse(jsonStr);
}
public static (string, double) DeserializeDynamic(string jsonStr)
{
var dyn = DynamicJson.Parse(jsonStr);
return ((string)dyn.timestamp, (double)dyn.last_price.price);
}
}
###6.Utf8Json
dynamicの場合はインデクサを使います(.変数名は例外発生)。今後のアップデートで変わるかもしれません。
using Utf8Json;
public static class Utf8Json
{
public static Data Deserialize(string strJson)
{
return JsonSerializer.Deserialize<Data>(strJson);
}
public static (string, double) DeserializeDynamic(string strJson)
{
var dyn = JsonSerializer.Deserialize<dynamic>(strJson);
return ((string)dyn["timestamp"], (double)dyn["last_price"]["price"]);//インデクサを使う
}
}
##デシリアライズ時間の比較
1.逐次処理
以下のコードで比較します。Zip→txtListの続きからです。
//DataContractJsonSerializer
sw.Restart();
foreach (var str in txtList)
{
DataContractJsonSerialize.Deserialize(str);
}
sw.Stop();
Console.WriteLine("DataContractJsonSerializer\t" + sw.Elapsed);
//JsonNet
sw.Restart();
foreach (var str in txtList)
{
JsonNet.Deserialize(str);
}
sw.Stop();
Console.WriteLine("JsonNet\t" + sw.Elapsed);
sw.Restart();
foreach (var str in txtList)
{
JsonNet.DeserializeDynamic(str);
}
sw.Stop();
Console.WriteLine("JsonNet(dynamic)\t" + sw.Elapsed);
//ServiceStack.Text
sw.Restart();
foreach (var str in txtList)
{
ServiceStackJson.Deserialize(str);
}
sw.Stop();
Console.WriteLine("ServiceStack.Text\t" + sw.Elapsed);
sw.Restart();
foreach (var str in txtList)
{
ServiceStackJson.DeserializeDynamic(str);
}
sw.Stop();
Console.WriteLine("ServiceStack.Text(dynamic)\t" + sw.Elapsed);
//Jil
sw.Restart();
foreach (var str in txtList)
{
JilJson.Deserialize(str);
}
sw.Stop();
Console.WriteLine("Jil\t" + sw.Elapsed);
sw.Restart();
foreach (var str in txtList)
{
JilJson.DeserializeDynamic(str);
}
sw.Stop();
Console.WriteLine("Jil(dynamic)\t" + sw.Elapsed);
//DynamicJSON
sw.Restart();
foreach (var str in txtList)
{
DynamicJsonDeserialize.Deserialize(str);
}
sw.Stop();
Console.WriteLine("DynamicJSON\t" + sw.Elapsed);
sw.Restart();
foreach (var str in txtList)
{
DynamicJsonDeserialize.DeserializeDynamic(str);
}
sw.Stop();
Console.WriteLine("DynamicJSON(dynamic)\t" + sw.Elapsed);
//Utf8Json
sw.Restart();
foreach (var str in txtList)
{
Utf8Json.Deserialize(str);
}
sw.Stop();
Console.WriteLine("Utf8Json\t" + sw.Elapsed);
sw.Restart();
foreach (var str in txtList)
{
Utf8Json.DeserializeDynamic(str);
}
sw.Stop();
Console.WriteLine("Utf8Json(dynamic)\t" + sw.Elapsed);
本当はtimestampとlast_priceを記録するはずでしたが、純粋にパフォーマンスを見たかったので端折っています。これを5回繰り返します。結果は次の通り。
逐次処理 | 1回目 | 2回目 | 3回目 | 4回目 | 5回目 | 平均 | 最大 | 最小 |
---|---|---|---|---|---|---|---|---|
Zip load | 00:50.981 | 00:50.870 | 00:50.253 | 00:50.896 | 00:50.770 | 00:50.754 | 00:50.981 | 00:50.253 |
DataContractJsonSerializer | 01:13.843 | 01:15.788 | 01:13.863 | 01:13.945 | 01:12.715 | 01:14.031 | 01:15.788 | 01:12.715 |
JsonNet | 00:43.019 | 00:43.401 | 00:42.688 | 00:43.640 | 00:42.800 | 00:43.110 | 00:43.640 | 00:42.688 |
ServiceStack.Text | 00:06.125 | 00:05.928 | 00:05.883 | 00:06.037 | 00:05.946 | 00:05.984 | 00:06.125 | 00:05.883 |
Jil | 00:06.825 | 00:06.749 | 00:07.059 | 00:06.951 | 00:06.858 | 00:06.888 | 00:07.059 | 00:06.749 |
DynamicJSON | 02:21.014 | 02:20.810 | 02:21.179 | 02:20.486 | 02:17.193 | 02:20.136 | 02:21.179 | 02:17.193 |
Utf8Json | 00:13.312 | 00:13.498 | 00:13.459 | 00:13.685 | 00:14.240 | 00:13.639 | 00:14.240 | 00:13.312 |
JsonNet(dynamic) | 01:40.964 | 01:41.524 | 01:40.885 | 01:41.389 | 01:44.052 | 01:41.763 | 01:44.052 | 01:40.885 |
ServiceStack.Text(dynamic) | 00:15.938 | 00:15.915 | 00:15.838 | 00:15.799 | 00:15.865 | 00:15.871 | 00:15.938 | 00:15.799 |
Jil(dynamic) | 01:03.895 | 01:03.903 | 01:03.662 | 01:05.012 | 01:03.606 | 01:04.016 | 01:05.012 | 01:03.606 |
DynamicJSON(dynamic) | 02:14.418 | 02:18.594 | 02:18.907 | 02:15.051 | 02:16.810 | 02:16.756 | 02:18.907 | 02:14.418 |
Utf8Json(dynamic) | 00:53.832 | 00:54.887 | 00:54.959 | 00:55.590 | 00:53.505 | 00:54.555 | 00:55.590 | 00:53.505 |
この例ではUtf8Jsonが最速にはなりませんでした。型を指定した場合、ServiceStackが6秒弱で最速、Jilも同じくらい速く7秒弱、そこから倍の6秒程度遅れてUtf8Jsonが13.6秒、そこから30秒程度とだいぶ遅れてJson.Netが43秒。.Net標準のDataContractJsonSerializerはJson.Netからさらに30秒程度遅れて1分14秒。DynamicJSONは一番遅く、2分オーバー。ただ、他のライブラリがdynamicでデシリアライズしたときに軒並み遅くなっているのに対し(おそらく全部のパラメーターを内部で一度パースしているのかと)、DynamicJSONはほぼ変わらないどころか、dynamicのほうが速いまでもあるので、型指定のクラス側で全部パースする場合はそこまで見劣りしないのかと思われます。
比較用にZipの読み込み時間を置いておきましたが、JSONシリアライザーの選択によってZipの読み込み1倍~2倍分の処理時間の差が出ます。本当はZip部分を高速化したかったけどそれはまた別な投稿で。
※追記:いろいろ試してみましたが、MessagePack C#を使うのが良さそうです。気力があれば書きます。
dynamicの場合はServiceStackの15秒以外はだいたい1分オーバー。かろうじてUtf8Jsonが55秒と1分割っているぐらいです。こう見るとServiceStack一強のように見えますが、ServiceStackの場合はdynamicの場合doubleが文字列で記録されているので、キャストに苦労するかもしれません。ServiceStackがdynamicの場合に明らかに速いのは、利便性を犠牲にして高速化しているからかもしれません。
この例の場合はStringの内部エンコードがUTF-16であるため、Stringのまま読み込ませているとUTF-8へ余計なエンコード変換が入るのがUtf8Jsonにとっていけないのかもしれません。そこでUtf8Jsonの場合のみ、stringではなくbyte[]を読ませるように改良してみます。
var byteList = new List<byte[]>();
var sw = new Stopwatch();
sw.Start();
foreach(var z in zips)
{
using (var archive = ZipFile.OpenRead(z))
{
foreach (var entry in archive.Entries)
{
using (var sr = new StreamReader(entry.Open()))
{
//txtList.Add(sr.ReadToEnd());
using (var ms = new MemoryStream())
{
sr.BaseStream.CopyTo(ms);
byteList.Add(ms.ToArray());
}
}
}
}
}
sw.Stop();
Console.WriteLine("Zip load\t" + sw.Elapsed);
Console.WriteLine("Memory : " + Environment.WorkingSet.ToString("N0") + "bytes");
sw.Restart();
foreach (var bytes in byteList)
{
var x = Utf8Json.DeserializeBytes(bytes);
}
sw.Stop();
Console.WriteLine("Utf8Json(Bytes)\t" + sw.Elapsed);
public static class Utf8Json
{
public static Data DeserializeBytes(byte[] byteJson)
{
return JsonSerializer.Deserialize<Data>(byteJson);
}
}
これを3回ほど実行してみました。
Zip load 00:00:37.8030819
Memory : 1,605,480,448bytes
Utf8Json(Bytes) 00:00:08.7113992
Zip load 00:00:37.7465432
Memory : 1,606,115,328bytes
Utf8Json(Bytes) 00:00:08.6711459
Zip load 00:00:38.4523601
Memory : 1,606,238,208bytes
Utf8Json(Bytes) 00:00:08.7258462
キャッシュ方式をstring→byte[]に変えたところ、Zipの展開時間が50秒→38秒と12秒ほど短縮され(24%減)、メモリ使用量が3.19Gbytes→1.61Gbytesと半分になり、Utf8Jsonのデシリアライズ時間が13.6秒→8.7秒と5秒ほど短縮され(36%減)ました。これでもまだ、stringの場合のServiceStackとJilのケースを抜くことができなかったのですが、特にメモリ減少の効果が大きそうです。JSONだとUTF-16までエンコードを拡大するメリットが無い(JSONの規則上エスケープされる)ので、UTF-8 byte[]で読み込んでしまうという作戦はかなり有用かもしれません。
ちなみにJilもServiceStackもTextReaderやStreamを引数として渡すことはできるものの、byte[]を直接渡すことができるのはざっと見たところUtf8Jsonだけでした。細かいチューニングを気にする場合、トータルで見たときにUtf8Jsonが優位に立てる可能性は十分にあります。
###2.並列処理
1.のstringの例を並列化してみます。Parallel.ForEachを使うだけです。ただし、並列化する場合は読み込む順番の保証がされなくなるので、結果が時系列で欲しい場合は最終的にソートする、スレッドセーフなコレクションを使って記録するなどの工夫が必要になります。Utf8Jsonの場合はこうします(他も同様なので省略します)。
var option = new ParallelOptions { MaxDegreeOfParallelism = 4 };
Parallel.ForEach(txtList, option, str =>
{
Utf8Json.Deserialize(str);
});
MaxDegreeOfParallelismを指定しないときりがなく並列していって死ぬので、スレッド数は4に制限しました。結果は次のようになりました。
並列処理 | 1回目 | 2回目 | 3回目 | 4回目 | 5回目 | 平均 | 最大 | 最小 |
---|---|---|---|---|---|---|---|---|
DataContractJsonSerializer | 00:28.103 | 00:26.502 | 00:27.138 | 00:28.102 | 00:27.326 | 00:27.434 | 00:28.103 | 00:26.502 |
JsonNet | 00:15.382 | 00:15.428 | 00:15.336 | 00:16.527 | 00:16.030 | 00:15.741 | 00:16.527 | 00:15.336 |
ServiceStack.Text | 00:02.466 | 00:02.779 | 00:02.317 | 00:02.115 | 00:02.460 | 00:02.427 | 00:02.779 | 00:02.115 |
Jil | 00:02.474 | 00:02.395 | 00:02.661 | 00:02.825 | 00:02.686 | 00:02.608 | 00:02.825 | 00:02.395 |
DynamicJSON | 00:49.005 | 00:50.380 | 00:47.114 | 00:52.129 | 00:48.747 | 00:49.475 | 00:52.129 | 00:47.114 |
Utf8Json | 00:05.365 | 00:05.307 | 00:04.810 | 00:05.250 | 00:05.298 | 00:05.206 | 00:05.365 | 00:04.810 |
JsonNet(dynamic) | 00:35.265 | 00:34.807 | 00:35.518 | 00:38.748 | 00:35.058 | 00:35.879 | 00:38.748 | 00:34.807 |
ServiceStack.Text(dynamic) | 00:05.630 | 00:05.543 | 00:05.617 | 00:05.714 | 00:05.582 | 00:05.617 | 00:05.714 | 00:05.543 |
Jil(dynamic) | 00:21.534 | 00:21.296 | 00:21.354 | 00:23.030 | 00:20.929 | 00:21.629 | 00:23.030 | 00:20.929 |
DynamicJSON(dynamic) | 00:47.160 | 00:49.797 | 00:46.207 | 00:46.603 | 00:46.825 | 00:47.318 | 00:49.797 | 00:46.207 |
Utf8Json(dynamic) | 00:20.059 | 00:19.494 | 00:19.764 | 00:19.649 | 00:20.118 | 00:19.817 | 00:20.118 | 00:19.494 |
試行間でそこまでぶれがないので、逐次処理の平均と並列処理の平均を比較してみます。
ライブラリ | 逐次平均 | 並列平均 | 逐次÷並列 | 逐次-並列 |
---|---|---|---|---|
DataContractJsonSerializer | 01:14.031 | 00:27.434 | 2.698 | 00:46.597 |
JsonNet | 00:43.110 | 00:15.741 | 2.739 | 00:27.369 |
ServiceStack.Text | 00:05.984 | 00:02.427 | 2.465 | 00:03.556 |
Jil | 00:06.888 | 00:02.608 | 2.641 | 00:04.280 |
DynamicJSON | 02:20.136 | 00:49.475 | 2.832 | 01:30.661 |
Utf8Json | 00:13.639 | 00:05.206 | 2.620 | 00:08.433 |
JsonNet(dynamic) | 01:41.763 | 00:35.879 | 2.836 | 01:05.884 |
ServiceStack.Text(dynamic) | 00:15.871 | 00:05.617 | 2.825 | 00:10.254 |
Jil(dynamic) | 01:04.016 | 00:21.629 | 2.960 | 00:42.387 |
DynamicJSON(dynamic) | 02:16.756 | 00:47.318 | 2.890 | 01:29.438 |
Utf8Json(dynamic) | 00:54.555 | 00:19.817 | 2.753 | 00:34.738 |
逐次÷並列を時間比、逐次-並列を時間差とします。比のほうは2.45~3.0の範囲内です。4並列にしても額面通り速度が4倍になることはないので、妥当といえるでしょう。差が大きいケースほど比も大きくなることが多く、これは極端に速いケースでは、JSONデシリアライズ以外の関係ない処理(foreachや文字列の読み込み)が全体に占めるウェイトが高くなるからだと思われます。逆に差が大きいのに比でそこまで伸びていないケースは、もともとの計算負荷が高いゆえに並列化してもCPUリソース食い尽くしやすく、あまり好ましくないと言えるかもしれません。
ここで回帰直線から大きく上に飛び出いている例は、DataContractJsonSerializerの2例と、DynamicJSONの2例です。この2つは単純に処理時間を見ても選びづらいです。
###3.逐次処理+Streamを引数にする
今までデシリアライズの際にstringやbyte[]を引数として渡していましたが、Zipアーカイブの各エントリーのStreamを渡す場合を考えます。したがってtxtListは必要なくなり、エントリーからダイレクトにデシリアライズするので、メモリ効率は遥かに良くなります。より実践的な方法です。これまで特に良好だった、ServiceStack, Jil, Utf8Jsonの3つについて比較をします。
var sw = new Stopwatch();
sw.Start();
foreach (var z in zips)
{
using (var archive = ZipFile.OpenRead(z))
{
foreach (var entry in archive.Entries)
{
using (var sr = new StreamReader(entry.Open()))
{
ServiceStack.Text.JsonSerializer.DeserializeFromStream<Data>(sr.BaseStream);//ServiceStackの場合
//Jil.JSON.Deserialize<Data>(sr);//Jilの場合
//Utf8Json.JsonSerializer.Deserialize<Data>(sr.BaseStream);//Utf8Jsonの場合
}
}
}
}
sw.Stop();
Console.WriteLine("ServiceStack(stream)\t" + sw.Elapsed);
各5回やった場合の結果は以下の通りです。
逐次 / stream | 1回目 | 2回目 | 3回目 | 4回目 | 5回目 | 平均 | 最大 | 最小 |
---|---|---|---|---|---|---|---|---|
ServiceStack(stream) | 00:40.706 | 00:41.131 | 00:40.793 | 00:40.724 | 00:40.230 | 00:40.717 | 00:41.131 | 00:40.230 |
Jil(stream) | 00:49.434 | 00:49.169 | 00:49.786 | 00:49.728 | 00:49.609 | 00:49.545 | 00:49.786 | 00:49.169 |
Utf8Json(stream) | 00:38.752 | 00:39.733 | 00:39.731 | 00:39.715 | 00:39.449 | 00:39.476 | 00:39.733 | 00:38.752 |
この例では、Utf8Jsonが(無事?)最速になりました。ほとんど処理時間にブレがないので、引数をstringにする場合とstreamにする場合で処理時間の平均値を比較します。
逐次処理 | 平均(stream) | 平均(string) | zip load + string - stream |
---|---|---|---|
ServiceStack | 00:40.717 | 00:05.984 | 00:16.021 |
Jil | 00:49.545 | 00:06.888 | 00:08.097 |
Utf8Json | 00:39.476 | 00:13.639 | 00:24.917 |
「zip load」はStringとしてZipを読み込んだ場合のZipをすべて読み込む時間の平均値50.754秒を足しています。「zip load + string - stream」はこれにstringを引数にJSONをデシリアライズしたときの平均処理時間を足し、streamを引数にJSONをデシリアライズした平均処理時間を引いたものです。ここでの処理の差はstream→stringの変換をするかどうか、txtListにstringを格納するかどうか(zip load, stringの場合は格納しているが、streamを直接渡した場合は格納する必要がない)、txtListをforeachで回すかどうか、とすべて定数項で表されそうな処理ですが、ライブラリによってこの差が一定ではないので、引数がstringの場合、streamの場合でそれぞれデシリアライズに最適化が入っていると思われます。Jilはstringに強く、Utf8Jsonはstreamに強いことがわかります。ServiceStackは傾向としてはUtf8Jsonに近そうです。
※追記(2/2)
entry.Open()で直接Stream読ませればいいところをStreamReaderをかませるという間抜けをしてしまったので、ダイレクトにEntryから読み込ませて再実験しました。JilについてはTextReader(StreamReaderが継承している抽象クラス)が引数なので除外し、ServiceStackとUtf8Jsonの比較を行います。
sw.Start();
foreach (var z in zips)
{
using (var archive = ZipFile.OpenRead(z))
{
foreach (var entry in archive.Entries)
{
ServiceStack.Text.JsonSerializer.DeserializeFromStream<Data>(entry.Open());
//Utf8Json.JsonSerializer.Deserialize<Data>(entry.Open());
}
}
}
sw.Stop();
Console.WriteLine("ServiceStack(Direct)" + sw.Elapsed);
sw.Restart();
foreach (var z in zips)
{
using (var archive = ZipFile.OpenRead(z))
{
foreach (var entry in archive.Entries)
{
using (var sr = new StreamReader(entry.Open()))
{
ServiceStack.Text.JsonSerializer.DeserializeFromStream<Data>(sr.BaseStream);
//Utf8Json.JsonSerializer.Deserialize<Data>(sr.BaseStream); //Utf8Json.JsonSerializer.Deserialize<Data>(sr.BaseStream);//Utf8Jsonの場合
}
}
}
}
sw.Stop();
Console.WriteLine("ServiceStack(StreamReader)" + sw.Elapsed);
Direct vs StreamReader | 平均 | 最大 | 最小 | 1回目 | 2回目 | 3回目 | 4回目 | 5回目 |
---|---|---|---|---|---|---|---|---|
ServiceStack(Direct) | 00:37.564 | 00:38.108 | 00:36.921 | 00:38.108 | 00:36.921 | 00:37.532 | 00:38.093 | 00:37.166 |
ServiceStack(StreamReader) | 00:37.714 | 00:38.675 | 00:36.956 | 00:38.675 | 00:36.956 | 00:37.402 | 00:37.501 | 00:38.037 |
Utf8Json(Direct) | 00:36.915 | 00:37.461 | 00:36.234 | 00:37.189 | 00:36.234 | 00:36.773 | 00:36.919 | 00:37.461 |
Utf8Json(StreamReader) | 00:36.590 | 00:37.017 | 00:36.231 | 00:36.231 | 00:36.253 | 00:36.846 | 00:36.603 | 00:37.017 |
初回にライブラリのロードが入ってるので、逆に遅くなっているかもしれません。基本的にStreamReaderを噛ませたほうが速くなるということはないはずです。この場合は0.1~0.5秒程度、entry.Open()したほうが速いといえるでしょう。(byte[]を変数で取る目的でもなく、StreamReader噛ませてたのがタダの間抜けなだけ)
コメントで指摘してくださった方、ありがとうございました。
まとめ
以上、ギガバイトクラスのJSONを読み込んでデシリアライズのパフォーマンスを計測しました。この例ではUtf8Jsonがぶっちぎりで強いという結論にはなりませんでしたが、特定の状況下では最速になりました。次のような結論になります。
- Jil, Utf8Json, ServiceStack.Text→どれも優秀。型を指定する場合はServiceStackが最速だが、指定しない場合は型のキャストでハマる可能性はある。Jilは安定して強く、stringからデシリアライズするときに最速。Utf8Jsonはstreamやbyte[]から読み込ませる場合は最速。stringからデシリアライズするのが無駄というのが理解できれば最も性能を引き出せるかもしれない。
- Json.Net→良くも悪くも普通
- DataContractJsonSerializer→配布用バイナリに余計なDLLが同梱されないメリットはあるが使いづらい
- DynamicJSON→7年前のライブラリだしそっとしておいてUtf8Jsonを使ってあげてください
今後のアップデートで大きく変わる可能性があるので、期待しましょう。
すっかり忘れていましたが、これが作りたかったビットコインの2018/1/18~1/24の6日分のティックチャートです。csvで出力してExcelでグラフ化しました。