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評比程式

    class Program
    {
        static void Main(string[] args)
        {
            //CreateSerializedData();
            TestJsonNet();
            TestJil();
        }

        private static void TestJil()
        {
            //隨機假造20萬筆User資料
            List<User> bigList = GenSimData();
            string fileName = "serialized.data";
            int indexToTest = 1024; //用來比對測試的筆數
            //序列化前取出第indexToTest筆資料的顯示內容
            string beforeSer = bigList[indexToTest].Display, afterDeser = null;

            Stopwatch sw = new Stopwatch();
            sw.Start();
            //將List<User> JSON化
            string json1 = JSON.Serialize(bigList);
            //string json1 = JSON.Serialize<List<User>>(bigList);
            //string json1 = JSON.SerializeDynamic(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 = (JSON.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 void TestJsonNet()
        {
            //隨機假造20萬筆User資料
            List<User> bigList = GenSimData();
            string fileName = "serialized.data";
            int indexToTest = 1024; //用來比對測試的筆數
            //序列化前取出第indexToTest筆資料的顯示內容
            string beforeSer = bigList[indexToTest].Display, afterDeser = null;

            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);

            //比對還原後的資料是否相同
            Console.WriteLine("Before: {0}", beforeSer);
            Console.WriteLine("After: {0}", afterDeser);
            Console.WriteLine("Pass Test: {0}", beforeSer.Equals(afterDeser));
            Console.Read();
        }

        private static void CreateSerializedData()
        {
            //隨機假造20萬筆User資料
            List<User> bigList = GenSimData();
            string fileName = "serialized.data";
            int indexToTest = 1024; //用來比對測試的筆數
            //序列化前取出第indexToTest筆資料的顯示內容
            string beforeSer = bigList[indexToTest].Display, afterDeser = null;

            DataContractSerializer dcs = new DataContractSerializer(bigList.GetType());
            Stopwatch sw = new Stopwatch();
            sw.Start();
            //將List<User>序列化後寫入檔案
            using (FileStream stm = new FileStream(fileName, FileMode.Create))
            {
                dcs.WriteObject(stm, bigList);
            }
            //using (FileStream stm = new FileStream(fileName, FileMode.Create))
            //{
            //    //用GZipStream把FileStream包起來
            //    using (GZipStream zip = new GZipStream(stm, CompressionMode.Compress))
            //    {
            //        //序列化結果改寫入GZipStream
            //        dcs.WriteObject(zip, 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 = (dcs.ReadObject(stm) as List<User>)[indexToTest].Display;
            }
            //using (FileStream stm = new FileStream(fileName, FileMode.Open))
            //{
            //    //一樣用GZipStream把FileStream包起來
            //    using (GZipStream zip = new GZipStream(stm, CompressionMode.Decompress))
            //    {
            //        //還原的二進位資料來源改為GZipStream
            //        afterDeser = (dcs.ReadObject(zip) as List<User>)[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 < 200000; 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);
                }
            }
        }

    }  
 

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選項來修正:

    private static void JilDateTime()
    {
        var dt = DateTime.Now;

        Console.WriteLine(JSON.Serialize(dt));
        // 二種指定 Options 選擇方式
        Console.WriteLine(JSON.Serialize(dt, new Options(dateFormat: Jil.DateTimeFormat.ISO8601)));
        Console.WriteLine(JSON.Serialize(dt, Options.ISO8601));

        Console.WriteLine(JSON.Deserialize<DateTime>(JSON.Serialize(dt)).ToLocalTime());
        Console.Read();
    }  
 

"\/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()來修正時區問題:

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

重新跑一次測試程式:

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

沒有留言:

張貼留言

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