JSON(反)序列化之唯快不破新選擇-Jil

JSON(反)序列化之唯快不破新選擇-Jil

天下武功-唯快不破

ASP.NET MVC的ActionResult一直有個讓人疑惑的地方,就是它的JsonResult,JsonResult預設使用了.NET Framework的JavaScriptSerializer類別,並不是說JavaScriptSerializer不好,每一個技術都有它的時空背景,.NET Framework發展過程中JSON格式也是慢慢才成為標準。在JSON格式流行與大量使用下,JavaScriptSerializer已經有點上氣不接下氣。而微軟在ASP.NET Web API第1版中就非常明確的以JSON.NET取而代之。而ASP.NET MVC到第5版為止雖然一直無異動這部分的程式碼,原因也很簡單,ASP.NET MVC設計了優良的擴充點,讓開發者想要擴充原有的ActionResult也不是太難的問題。

擴充為JsonNetResult

如果讀者想使用Json.NET來擴充原有的ActionResult,可以參考Json.NET的作者James所寫的JsonNetResult或是參考黑大所寫的JsonNetController版本。這都是能直接上線使用的程式碼,我就不再多說明。

我就是速度(I am speed)

最近在進行專案效能調教,在MVC專案中使用了大量的JsonResult,在黑大JSON轉換效能評比-Json.NET,就決定是你了!得到JavaScriptSerializer早就應該退休了的結論,取代ASP.NET MVC預設JsonResult是個快又有效的一步。不過在這個唯快不破的時代,我想找找是否還有更好的選擇。

我發現一個JSON(反)序列化新選擇-Jil

以下效能圖表參考原專案:

Serialization for  comparison chat 1
Serialization for  comparison chat 2
Serialization for  comparison chat 3

從圖表可以發現,我們最常使用的JSON.NET表現可以說是中規中矩,但Jil速度上更勝一籌。(另一套由Google發展的Protobuf,速度可以說是第一名,但我沒有考慮是因為它在nuget上停在2013年就無更新了,而Jil在我撰文期間都還收到更新的通知。)

不過那是別人的圖表,我還是半信半疑,我決定用黑大的效能評比程式做一個Jil的評比:

Json.net與Jil評比程式

001class Program
002{
003    static void Main(string[] args)
004    {
005        //CreateSerializedData();
006        TestJsonNet();
007        TestJil();
008    }
009 
010    private static void TestJil()
011    {
012        //隨機假造20萬筆User資料
013        List<User> bigList = GenSimData();
014        string fileName = "serialized.data";
015        int indexToTest = 1024; //用來比對測試的筆數
016        //序列化前取出第indexToTest筆資料的顯示內容
017        string beforeSer = bigList[indexToTest].Display, afterDeser = null;
018 
019        Stopwatch sw = new Stopwatch();
020        sw.Start();
021        //將List<User> JSON化
022        string json1 = JSON.Serialize(bigList);
023        //string json1 = JSON.Serialize<List<User>>(bigList);
024        //string json1 = JSON.SerializeDynamic(bigList);
025        sw.Stop();
026        Console.WriteLine("Serialization: {0:N0}ms", sw.ElapsedMilliseconds);
027        sw.Reset();
028        sw.Start();
029        //由檔案字串反序列化還原回List<User>
030        using (FileStream stm = new FileStream(fileName, FileMode.Open))
031        {
032            //還原後一樣取出第indexToTest筆的User顯示內容
033            afterDeser = (JSON.Deserialize<List<User>>(json1))
034                         [indexToTest].Display;
035        }
036        sw.Stop();
037        Console.WriteLine("Deserialization: {0:N0}ms", sw.ElapsedMilliseconds);
038 
039        //比對還原後的資料是否相同
040        Console.WriteLine("Before: {0}", beforeSer);
041        Console.WriteLine("After: {0}", afterDeser);
042        Console.WriteLine("Pass Test: {0}", beforeSer.Equals(afterDeser));
043        Console.Read();
044    }
045 
046    private static void TestJsonNet()
047    {
048        //隨機假造20萬筆User資料
049        List<User> bigList = GenSimData();
050        string fileName = "serialized.data";
051        int indexToTest = 1024; //用來比對測試的筆數
052        //序列化前取出第indexToTest筆資料的顯示內容
053        string beforeSer = bigList[indexToTest].Display, afterDeser = null;
054 
055        Stopwatch sw = new Stopwatch();
056        sw.Start();
057        //將List<User> JSON化
058        string json1 = JsonConvert.SerializeObject(bigList);
059        sw.Stop();
060        Console.WriteLine("Serialization: {0:N0}ms", sw.ElapsedMilliseconds);
061        sw.Reset();
062        sw.Start();
063        //由檔案字串反序列化還原回List<User>
064        using (FileStream stm = new FileStream(fileName, FileMode.Open))
065        {
066            //還原後一樣取出第indexToTest筆的User顯示內容
067            afterDeser = (JsonConvert.DeserializeObject<List<User>>(json1))
068                         [indexToTest].Display;
069        }
070        sw.Stop();
071        Console.WriteLine("Deserialization: {0:N0}ms", sw.ElapsedMilliseconds);
072 
073        //比對還原後的資料是否相同
074        Console.WriteLine("Before: {0}", beforeSer);
075        Console.WriteLine("After: {0}", afterDeser);
076        Console.WriteLine("Pass Test: {0}", beforeSer.Equals(afterDeser));
077        Console.Read();
078    }
079 
080    private static void CreateSerializedData()
081    {
082        //隨機假造20萬筆User資料
083        List<User> bigList = GenSimData();
084        string fileName = "serialized.data";
085        int indexToTest = 1024; //用來比對測試的筆數
086        //序列化前取出第indexToTest筆資料的顯示內容
087        string beforeSer = bigList[indexToTest].Display, afterDeser = null;
088 
089        DataContractSerializer dcs = new DataContractSerializer(bigList.GetType());
090        Stopwatch sw = new Stopwatch();
091        sw.Start();
092        //將List<User>序列化後寫入檔案
093        using (FileStream stm = new FileStream(fileName, FileMode.Create))
094        {
095            dcs.WriteObject(stm, bigList);
096        }
097        //using (FileStream stm = new FileStream(fileName, FileMode.Create))
098        //{
099        //    //用GZipStream把FileStream包起來
100        //    using (GZipStream zip = new GZipStream(stm, CompressionMode.Compress))
101        //    {
102        //        //序列化結果改寫入GZipStream
103        //        dcs.WriteObject(zip, bigList);
104        //    }
105        //}
106 
107        sw.Stop();
108        Console.WriteLine("Serialization: {0:N0}ms", sw.ElapsedMilliseconds);
109        sw.Reset();
110        sw.Start();
111        //由檔案反序列化還原回List<User>
112        using (FileStream stm = new FileStream(fileName, FileMode.Open))
113        {
114            //還原後一樣取出第indexToTest筆的User顯示內容
115            afterDeser = (dcs.ReadObject(stm) as List<User>)[indexToTest].Display;
116        }
117        //using (FileStream stm = new FileStream(fileName, FileMode.Open))
118        //{
119        //    //一樣用GZipStream把FileStream包起來
120        //    using (GZipStream zip = new GZipStream(stm, CompressionMode.Decompress))
121        //    {
122        //        //還原的二進位資料來源改為GZipStream
123        //        afterDeser = (dcs.ReadObject(zip) as List<User>)[indexToTest].Display;
124        //    }
125        //}
126        sw.Stop();
127        Console.WriteLine("Deserialization: {0:N0}ms", sw.ElapsedMilliseconds);
128 
129        //比對還原後的資料是否相同
130        Console.WriteLine("Before: {0}", beforeSer);
131        Console.WriteLine("After: {0}", afterDeser);
132        Console.WriteLine("Pass Test: {0}", beforeSer.Equals(afterDeser));
133        Console.Read();
134    }
135 
136    private static List<User> GenSimData()
137    {
138        List<User> lst = new List<User>();
139        Random rnd = new Random();
140        for (int i = 0; i < 200000; i++)
141        {
142            lst.Add(new User()
143            {
144                Id = Guid.NewGuid(),
145                RegDate = DateTime.Today.AddDays(-rnd.Next(5000)),
146                Name = "User" + i,
147                Score = rnd.Next(65535)
148            });
149        }
150        return lst;
151    }
152 
153    [Serializable]
154    private class User
155    {
156        public Guid Id { get; set; }
157        public DateTime RegDate { get; set; }
158        public string Name { get; set; }
159        public decimal Score { get; set; }
160        public string Display
161        {
162            get
163            {
164                return string.Format("{0} / {1:yyyy-MM-dd} / {2:N0}",
165                                     Name, RegDate, Score);
166            }
167        }
168    }
169 
170

Json.Net-20萬筆測試結果:

Serialization: 1,016ms
Deserialization: 1,051ms
Before: User1024 / 2013-12-26 / 61,284
After: User1024 / 2013-12-26 / 61,284
Pass Test: True

Jil-20萬筆測試結果:

Serialization: 901ms
Deserialization: 376ms
Before: User1024 / 2012-01-22 / 39,551
After: User1024 / 2012-01-21 / 39,551
Pass Test: False

Jil在序列化部分小勝JSON.NET,但在反序列化部分,那376ms是用倍級的差距獲勝,速度快到嚇人。自己測試取得的數據,無非為自己打了一針強心針。令人動心。

不過我們也最注意Jil的在最後反序列化的日期有些問題,造成測試失敗的結果。

Jil的日期

Jil在序列化時採用MicrosoftStyleMillisecondsSinceUnixEpoch,也就是和JavaScriptSerializer一樣的"\/Date(628318530718)\/"格式,這部分可以指定Options選項來修正:

01private static void JilDateTime()
02{
03    var dt = DateTime.Now;
04 
05    Console.WriteLine(JSON.Serialize(dt));
06    // 二種指定 Options 選擇方式
07    Console.WriteLine(JSON.Serialize(dt, new Options(dateFormat: Jil.DateTimeFormat.ISO8601)));
08    Console.WriteLine(JSON.Serialize(dt, Options.ISO8601));
09 
10    Console.WriteLine(JSON.Deserialize<DateTime>(JSON.Serialize(dt)).ToLocalTime());
11    Console.Read();
12

"\/Date(1455118355525)\/"
"2016-02-10T15:32:35.5258567Z"
"2016-02-10T15:32:35.5258567Z"
2016/2/10 下午 11:32:35

在反序列化時我加上了.ToLocalTime()以修改時區造成的時間不正確的問題。原因在官網的組態段落有說明:

ISO8601, a string

  • for DateTimes & DateTimeOffsets, ie. "2011-07-14T19:43:37Z"
    • DateTimes are always serialized in UTC (timezone offset = 00:00), because Local DateTimes cannot reliably roundtrip
    • DateTimeOffsets include their timezone offset when serialized
  • for TimeSpans, ie. "P40DT11H10M9.4S"

修正我們的Jil測試程式,除了在(反)序列化加上Options選擇外,在Display屬性加上.ToLocalTime()來修正時區問題:

1public string Display
2{
3    get
4    {
5        return string.Format(
6            "{0} / {1:yyyy-MM-dd} / {2:N0}",
7            Name, RegDate.ToLocalTime(), Score);
8    }
9}

重新跑一次測試程式:

Serialization: 991ms
Deserialization: 402ms
Before: User1024 / 2007-03-16 / 55,103
After: User1024 / 2007-03-16 / 55,103
Pass Test: True

不只速度,反序列化後的日期也正確無誤了。

小結

Jil(反)序列化就是以效能為主軸,速度的表現也非常可圈可點。時間格式是要注意的小地方,Options.ISO8601是我們比較習慣使用的格式。由時間問題我們也學習到,Jil在序列化如果碰到時間是會進行UTC時區處理,所以在反序列化時要記得還原成當地時間。

Jil效能通過了驗證,那麼下一集:「取代JsonResult新選擇-招喚JilResult」。

Github Sample Source code:KK.JilTest

系列文章

  1. JSON(反)序列化之唯快不破新選擇-Jil
  2. ASP.NET MVC-取代JsonResult新選擇-招喚JilResult
  3. ASP.NET Web API-取代JsonFormatter新選擇-招喚JilFormatter

沒有留言:

張貼留言

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。