專案裡有個小需求,Web API要以JSON格式傳回一個巨大物件(數十MB)。在.NET裡做JSON轉換,依我所知有三種選擇,JavaScriptSerializer、DataContractJsonSerializer及Json.NET。以前沒有想太多,覺得JavaScriptSerializer是.NET內建的,不像Json.NET還需要另外參照Library,又不像DataContractJsonSerializer得動用Stream、Encoding處理字串,應是最方便的做法,所以不少程式都用JavaScriptSerializer處理JSON轉換,長期下來除了日期格式的眉角,倒也沒什麼問題。
但這回在處理大型物件時,便突顯出JavaScriptSerializer的效能問題。用以下的範例來重現:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Web.Script.Serialization;
namespace ZipSer
{
internal class Program
{
private static void Main(string[] args)
{
//隨機假造2萬筆User資料
List<User> bigList = GenSimData();
string fileName = "serialized.data";
int indexToTest = 1024; //用來比對測試的筆數
//序列化前取出第indexToTest筆資料的顯示內容
string beforeSer = bigList[indexToTest].Display,
afterDeser = null;
JavaScriptSerializer jss = new JavaScriptSerializer();
//要提高上限,否則物件較大時會產生例外
jss.MaxJsonLength = int.MaxValue;
Stopwatch sw = new Stopwatch();
sw.Start();
//將List<User> JSON化
string json1 = jss.Serialize(bigList);
sw.Stop();
Console.WriteLine("Serialization: {0:N0}ms",
sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
//由檔案字串反序列化還原回List<User>
using (FileStream stm =
new FileStream(fileName, FileMode.Open))
{
//還原後一樣取出第indexToTest筆的User顯示內容
afterDeser =
(jss.Deserialize<List<User>>(json1))[indexToTest].Display;
}
sw.Stop();
Console.WriteLine("Deserialization: {0:N0}ms", sw.ElapsedMilliseconds);
//比對還原後的資料是否相同
Console.WriteLine("Before: {0}", beforeSer);
Console.WriteLine("After: {0}", afterDeser);
Console.WriteLine("Pass Test: {0}", beforeSer.Equals(afterDeser));
Console.Read();
}
private static List<User> GenSimData()
{
List<User> lst = new List<User>();
Random rnd = new Random();
for (int i = 0; i < 20000; i++)
{
lst.Add(new User()
{
Id = Guid.NewGuid(),
RegDate =
DateTime.Today.AddDays(-rnd.Next(5000)),
Name = "User" + i,
Score = rnd.Next(65535)
});
}
return lst;
}
[Serializable]
private class User
{
public Guid Id { get; set; }
public DateTime RegDate { get; set; }
public string Name { get; set; }
public decimal Score { get; set; }
public string Display
{
get
{
return string.Format(
"{0} / {1:yyyy-MM-dd} / {2:N0}",
Name, RegDate, Score);
}
}
}
}
}
程式是用前一篇序列化文章的範例修改的,一樣隨機產生一個2萬筆資料的List<User>,但改用JavaScriptSerializer執行JSON序列化及還原。
途中會先遇到一顆地雷,預設JavaScriptSerializer能處理的資料規模有上限,當資料物件大到一定程度(JSON字串超過4MB),就會發生以下錯誤:
Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.
因此,要調整MaxJsonLength屬性,很豪氣地一口氣設到int.MaxValue好了!
Serialization: 717ms
Deserialization: 65,844ms
Before: User1024 / 1999-03-28 / 3,749
After: User1024 / 1999-03-27 / 3,749
Pass Test: False
第二顆地雷出現了,測試的結果是False!! 這是以前提過的老問題。(DateTime經JavaScriptSerializer.Serialize()再JavaScriptSerializer.Deserialize()時會因時區標準不同,對台灣而言而產生8小時的時差,故1999-03-28 00:00:00會變成1999-03-27 16:00:00)
第三顆雷,瞎毁? 反序列化要65秒? 而且這還不是最誇張的,若試著把List<User>的陣列大小提高到30萬筆,jss.Deserialize()執行起來會沒完沒了,有種會一直到跑到天荒地老的fu... (至少已經遠超出我耐性的極限,沒等到結果我就卡歌了... 你知道的,身為一個中年程序員,可不想拿所剩不多的歲月跟它瞎耗) 想想,或許MaxJsonLength預設2097152是有原因的。
接著來試試DataContractJsonSerializer:
DataContractJsonSerializer dcjs =
new DataContractJsonSerializer(bigList.GetType());
Stopwatch sw = new Stopwatch();
sw.Start();
//將List<User> JSON化
MemoryStream ms = new MemoryStream();
dcjs.WriteObject(ms, bigList);
ms.Flush();
string json1 = Encoding.UTF8.GetString(ms.ToArray());
sw.Stop();
Console.WriteLine("Serialization: {0:N0}ms",
sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
//由檔案字串反序列化還原回List<User>
using (FileStream stm =
new FileStream(fileName, FileMode.Open))
{
//還原後一樣取出第indexToTest筆的User顯示內容
MemoryStream ms2 =
new MemoryStream(Encoding.UTF8.GetBytes(json1));
afterDeser =
((List<User>)dcjs.ReadObject(ms2))[indexToTest].Display;
}
sw.Stop();
Console.WriteLine("Deserialization: {0:N0}ms", sw.ElapsedMilliseconds);
結果合理多了,序列化及反序化都約在0.5秒完成! 也沒有發生日期轉換誤差。
Serialization: 459ms
Deserialization: 568ms
Before: User1024 / 2010-09-13 / 38,262
After: User1024 / 2010-09-13 / 38,262
Pass Test: True
壓軸上場,請廣受好評的Json.NET出來露一手:
Stopwatch sw = new Stopwatch();
sw.Start();
//將List<User> JSON化
string json1 = JsonConvert.SerializeObject(bigList);
sw.Stop();
Console.WriteLine("Serialization: {0:N0}ms",
sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
//由檔案字串反序列化還原回List<User>
using (FileStream stm =
new FileStream(fileName, FileMode.Open))
{
//還原後一樣取出第indexToTest筆的User顯示內容
afterDeser =
(JsonConvert.DeserializeObject<List<User>>(json1))
[indexToTest].Display;
}
sw.Stop();
Console.WriteLine("Deserialization: {0:N0}ms", sw.ElapsedMilliseconds);
測試成績出爐,與DataContractJsonSerializer相比,序列化速度慢一點點,但反序列化則快了不少:
Serialization: 536ms
Deserialization: 415ms
Before: User1024 / 2007-07-11 / 5,229
After: User1024 / 2007-07-11 / 5,229
Pass Test: True
綜合評量後,做個簡單結論:
- JavaScriptSerializer處理大型物件的效能令人髮指... (或許也是MaxJsonLength預設值不大的原因) 而日期時間還原後需要額外校正,看起來只適用於小型物件、沒有日期型別、不想加掛Library的場合。
- DataContractJsonSerializer屬BCL內建,雖然使用時必須動用MemoryStream,但也等於提供加入壓縮、加密或其他處理的方便管道,在某些情場下很有用。
- Json.NET的轉換語法最簡便(直接用static方法搞定,不需要建構物件),處理效能出色,日期時間格式預設也符合常見的ISO 8601標準(即2012-12-21T00:00:00Z),跟一些Client Library較無整合問題,還支援動態物件及其他附加特色,而以往被我嫌棄的需額外下載及加入參考的缺點,現在靠NuGet已能輕鬆解決,對我的需求而言,榮登最佳解決方案。
PS: 如想進一步了解,Json.NET網站有完整的功能比較表,也有一分跟JavaScriptSerializer、DataContractJsonSerializer的效能評測(不過,該案例應針對Json.NET的強項調整過,Json.NET的表現好得未免太嚇人 XD),還有說明文件(瀏覽文件後才發現Json.NET的功能跟擴充性真是踏馬的多),有興趣的朋友可以參考。