物件序列化之舉手之勞省空間

分享一下最近學會的序列化壓縮技巧。

情境如下,查詢資料庫後取得List<User>物件,打算透過序列化成檔案的方式保存,方便日後能快速還原回List<User>查詢比對,以達到離線使用的目標。

在.NET要玩序列化不過是小事一樁,只要針對類別建構出DataContractSerializer物件,再搭配FileStream,一個SerializeObject()指令就能將物件儲存成檔案,還原時也只要一個Read()指令就搞定,十分方便。

以下程式模擬了一個20萬筆資料的巨型集合物件 -- List<User>,以DataContractSerializer序列化為檔案,再反序列化回List<User>,並抽樣檢查還原的第1024筆資料,比對是否與序列化前相同,以確認資料正確無損。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
 
namespace ZipSer
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            //隨機假造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);
            }
            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;
            }
            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);
                }
            }
        }
    }
}

執行結果如下:

Serialization: 2,349ms
Deserialization: 2,953ms
Before: User1024 / 2001-07-22 / 43,320
After: User1024 / 2001-07-22 / 43,320
Pass Test: True

序列化及反序列化的速度都蠻快的,花不到3秒,但有個問題:

序列化後的serialized.data檔案大小高達78MB!! 檔案痴肥是開發人員不夠專業的象徵,所以就快來研究如何改善吧!

要減少序列化後的體積,自訂更有效率的序列化邏輯是一種解決;而我看到序列化後的內容接近文字形式,則有另一個點子,何不在序列化存檔前進行壓縮縮小體積;在還原時,則讀檔後先解壓縮再做反序列化? 在種內容接近文字的情境,應能省下可觀空間,實做起來也會比自訂序列化邏輯簡單許多。

簡單嘗試後,發現GZipStream的設計概念很棒,我們只需為FileStream包上一層GZipStream,然後將WriteObject()及Read()的資料串流對象由FileStream改成GZipStream,只改個兩行程式,馬上就有壓縮/解壓縮功能!!

    sw.Start();
    //將List<User>序列化後寫入檔案
    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))
    {
        //一樣用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);

測試結果,加入壓縮解壓程序後,執行時間略為增加(也只多出不到2秒),比對測試則驗證還原後資料無損。

Serialization: 4,072ms
Deserialization: 3,801ms
Before: User1024 / 2007-08-25 / 791
After: User1024 / 2007-08-25 / 791
Pass Test: True

接著來驗收壓縮成果,原本78.5MB的檔案,壓縮後只剩11.3MB,縮小為14%。

只用幾行程式,就讓序列化增加了壓縮功能,又到了該為.NET歡呼的時刻,讚啦!!!

歡迎推文分享:
Published 07 June 2012 11:26 PM 由 Jeffrey
Filed under: ,
Views: 17,575



意見

# Laneser said on 07 June, 2012 11:08 PM

這個 GzipStream 我只有遇到一個地雷...就是壓縮之前大小不能超過 4GB ...

# 蹂躪 said on 08 June, 2012 08:31 PM

那個限制在4.0就拿掉了

# Ammon said on 12 June, 2012 09:23 PM

為何不用 binary serialize?

# Jeffrey said on 13 June, 2012 06:18 AM

to Ammon, 有考慮過BinaryFormatter,但評估集合的資料物件有很大比例的屬性值是重複的,這部分只能透過壓縮來減少體積,使用Binary也幫不了太多忙。

# Litfal said on 28 August, 2012 07:22 AM

Deflate在C#裡面的大地雷不是4GB

是Block在太小時,壓縮率極差。

更甚者Block在某個大小時,壓縮後的體積反而誇張的增大。

(之前測試過,很奇怪的問題,用的block不大,好像是256左右,因為是測試用的程式碼,可能要找找看)

而用Stream傳遞給寫入器,最大的優點與缺點是封裝,隱藏寫入的細節,所以沒辦法知道寫入函式裡對於block如何處裡。

Serializer內容不清楚,雖然大多案例看起來沒問題,但總會害怕哪時忽然出事。

根據我實驗的結果,在建立GZipStream後,再用一層BufferedStream包起GZipStream,最後將BufferedStream指定給寫入器是比較通用的閃雷法。

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<June 2012>
SunMonTueWedThuFriSat
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication