2013年9月11日 星期三

[轉貼] JSON轉換效能評比 - Json.NET,就決定是你了!

出處:http://blog.darkthread.net/post-2012-06-09-json-net-performance.aspx

專案裡有個小需求,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
綜合評量後,做個簡單結論:
  1. JavaScriptSerializer處理大型物件的效能令人髮指... (或許也是MaxJsonLength預設值不大的原因) 而日期時間還原後需要額外校正,看起來只適用於小型物件、沒有日期型別、不想加掛Library的場合。
  2. DataContractJsonSerializer屬BCL內建,雖然使用時必須動用MemoryStream,但也等於提供加入壓縮、加密或其他處理的方便管道,在某些情場下很有用。
  3. 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的功能跟擴充性真是踏馬的多),有興趣的朋友可以參考。

沒有留言:

張貼留言