Unity)2017-09-29Utf8Jsonという新しいC#用のJSONシリアライザを作りました。4.5版もあります)。Unity用にもちゃんと用意しています。Star超えしました。C#で1位、全体で20位ぐらいになってたりました。使い方を説明してもしょうがないので(ReadMe見てね)、ここではパフォーマンスに関する実装面での工夫について説明します。赤枠で囲ったのがUtf8Jsonで、それより左側はバイナリシリアライザです。ウリは超高速性と、十分な拡張性。C#には敵わないのですが(というか改めて見てもむしろデタラメに速すぎ......)、他のJSONシリアライザよりも勝っています。シリアライズに至ってはprotobuf-netより速いし。メモリアロケーションも非常に少ない(基本的にpayloadのサイズ分しか必要とせず、メモリプールに収まる範囲内では、ゼロアロケーションです)。コンセプトの核はシンプルです。byte[]に直接読み書きすることで、バイナリシリアライザであるかのように動作させる。それにより、従来あったString(UTF16)との相互変換のオーバーヘッドを消して、速度を圧倒的に向上させることができる。このような試みは、corefxlabによりSpan<T>という、そろそろ標準に入りそうでまだ入ってない効率的な配列のスライスっぽい何か、の活用の一貫として研究されています。corefxlabのWikiにあるSystem.Text.Formattingの解説を見てみましょう。ToStringやFormattingを避け、直接UTF8として書き込むことにより、多くのアロケーションを避け、より高速に動作することを目指しています。software」ではありますが。汎用的なJSONシリアライザとはまた別のものです。いえ、コンセプトの正しさ、目指さなければならない地点はどこにあるか、というのは分かると思います。Utf8Jsonは、実装した結果を持って、そこに到達しました。Constantsなどの提案もありますが、実現するかも分からない遠い未来のことであり、UTF16のコストは払い続けなければならないでしょうね。null安全に関する話もそうですが、C#もレガシー言語と言わざるを得ない要素は色々と嵩んできてはいると思っています。Goように見えますし、しかし言語の大元に組み込まれているもの(UTF16)を変えるというのは非常に難しいところでしょう。その中で、しかし現実は現実として、今、このC#で、いかに、どこまでやれるかというのが勝負だし、C#を戦場で勝ち残れる環境に引き上げていくことでもあります。TextReader/Writerのオーバーヘッド通常のJSONシリアライザはstringを返しますが、別にstringを返されても使い道はないので、その後更にbyte[]に変換するでしょう、多くの場合はEncoding.UTF8.GetBytesにより。TextReader/WriterでStreamに書き込みするかの、二択です。そこに着目した場合、通常のJSONシリアライザにはオーバーヘッドが存在します。byte[]のほうが明らかに速そうだ、というのは単純明快でよくわかります。StreamWriter)はどうでしょう。ベンチマークで分かる通り、StreamWriterを介したものはStringからのbyte[]よりも、むしろ低速です。「ストリーミング」で良いかのように見えますが、それは見せかけだけのことで、実際には内部でバッファを"いい具合"に抱えてやりくりしているだけのことであり、更にそれによりStreamWriterへの書き込みそのものに多くのオーバーへッドが存在するからです。MVCのシリアライザを差し替えようとして、このような実装をついしてしまうでしょう。}}context.Response.BodyはStreamだから、普通にStreamWriter通して書きますよね?そのことにより謳い文句よりもずっと低速で、多くのメモリ消費をしてしまっているというのに!これが、Jilに差し替えても爆速だぜー、を達成できない理由です(とはいえさすがにもちろんJSON.NETよりは遥かに速い)。今も変わらず、JSONのシリアライゼーションは.NETのボトルネックであり続けているのです。いう、かなりデカいバッファをいきなり確保します。referencesource/streamwriter.cs#L203-L204。いうのがストリームの代償なんですよね、あばー。シリアライズの最適化こんな感じで動いています、の図。逆コンパイル結果のイメージ。jsonFormatterResolver)}この場合だと処理ステップ的には5ブロック分です。JSONのシリアライズが(バイナリに比べて)遅くなってしまう要因は色々あるのですが、各プロパティ名の書き込みには最適化の余地があります。一つに、名前は固定なので、事前にエンコードしておきましょう。区切り記号":"や連結","、ヘッダ"{"の出現位置は決まっているので、名前にくっつけて一体化してしまいます。パフォーマンス向上の基本原則は呼び出し回数を抑えること、なので一体化には大いに意味があります。あとは、ターゲットがbyte[]なので、メモリコピーするだけです。メモリコピーの最適化の問題に入ります。C#におけるコピーの手法として、Array.Copy、を卒業した人はBuffer.BlockCopyを使い出します。これはプリミティブ型のコピーでは、Array.Copyより高速という謳い文句で、概ね実際そうなのですが、小さいサイズのコピーの場合は話が少々違ってきます。プロパティ名は通常、かなり小さい(普通は10バイト以下、多くても30バイト以下でしょう)。そもそもBuffer.BlockCopyには無駄があります。coreclrに改善PRが出されているので、それを見るのが分かりやすいですが、Buffer.BlockCopyはランタイムのネイティブのC++コードの呼び出しになりますが、型のチェックと汎用的な型による処理が入っているんですね。いうのも、Buffer.BlockCopyはプリミティブ型全てがコピーできる代物だから。利用用途の9割はbyte[]のコピーのはずで、より最適なコードが叩き込めるはずです。いうわけで、2016年の2月に、これは入りました。それ以前のものに関しては南無、という話です。CLRの話なのでCoreじゃないCLRにどの程度反映されているかは謎です(多分、反映されてない気がする)。いえどちらにせよ使いません。4.6から追加されたBuffer.MemoryCopyのほうが高速だからです。中身を説明すると、ある程度のThreshold(x64では2048)までは、SSE2が使える環境なら64バイト単位(RyuJITがそうする)、そうじゃなければ8バイト単位でC#のunsafeで普通にコピーするという代物です。unsafeで普通にコピー。