[.NET]使用十年股价对比各种序列化技术

简介: 原文:[.NET]使用十年股价对比各种序列化技术1. 前言 上一家公司有搞股票,当时很任性地直接从服务器读取一个股票10年份的股价(还有各种指标)在客户端的图表上显示,而且因为是桌面客户端,传输的数据也是简单粗暴地使用Soap序列化。
原文: [.NET]使用十年股价对比各种序列化技术

1. 前言

上一家公司有搞股票,当时很任性地直接从服务器读取一个股票10年份的股价(还有各种指标)在客户端的图表上显示,而且因为是桌面客户端,传输的数据也是简单粗暴地使用Soap序列化。获取报价的接口大概如下,通过symbol、beginDate和endDate三个参数获取股票某个时间段的股价:

public IEnumerable<StockPrice> LoadStockPrices(string symbol,DateTime beginDate,DateTime endDate)
{
    //some code
}

后来用Xamarin.Forms做了移动客户端,在手机上就不敢这么任性了,移动端不仅对流量比较敏感,而且显示这么多数据也不现实,于是限制为不可以获取这么长时间的股价,选择一种新的序列化方式也被提上了日程。不过当时我也快离职了所以没关心这件事。
上周看到这篇问文章:【开源】C#.NET股票历史数据采集,【附18年历史数据和源代码】,一时兴起就试试用各种常用的序列化技术实现以前的需求。

2. 数据结构

[Serializable]
[ProtoContract]
[DataContract]
public class StockPrice
{
    [ProtoMember(1)]
    [DataMember]
    public double ClosePrice { get; set; }

    [ProtoMember(2)]
    [DataMember]
    public DateTime Date { get; set; }

    [ProtoMember(3)]
    [DataMember]
    public double HighPrice { get; set; }

    [ProtoMember(4)]
    [DataMember]
    public double LowPrice { get; set; }

    [ProtoMember(5)]
    [DataMember]
    public double OpenPrice { get; set; }

    [ProtoMember(6)]
    [DataMember]
    public double PrvClosePrice { get; set; }

    [ProtoMember(7)]
    [DataMember]
    public string Symbol { get; set; }

    [ProtoMember(8)]
    [DataMember]
    public double Turnover { get; set; }

    [ProtoMember(9)]
    [DataMember]
    public double Volume { get; set; }
}

上面是股价的数据结构,包含股票代号、日期、OHLC、前收市价(PreClosePice),成交额(Turnover)和成交量(Volume),这里我已经把序列化要用到的Attribute加上了。

测试数据使用長和(00001)2003年开始10年的股价,共2717条数据。为了方便测试已经把它们从数据库导出到文本文档。其实大小也就200K而已。

img_0fc4beaf7783ad094377da398beaa16f.png

3. 各种序列化技术

在.NET中要执行序列化有很多可以考虑的东西,如网络传输、安全性、.NET Remoting的远程对象等内容。但这里单纯只考虑序列化本身。

3.1 二进制序列化

二进制序列化将对象的公共字段和私有字段以及类(包括含有该类的程序集)的名称都转换成字节流,对该对象进行反序列化时,将创建原始对象的准确克隆。除了.NET可序列化的类型,其它类型要想序列化,最简单的方法是使用 SerializableAttribute 对其进行标记。

.NET中使用BinaryFormatter实现二进制序列化,代码如下:

public override byte[] Serialize(List<StockPrice> instance)
{
    using (var stream = new MemoryStream())
    {
        IFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, instance);
        return stream.ToArray();
    }
}


public override List<StockPrice> Deserialize(byte[] source)
{
    using (var stream = new MemoryStream(source))
    {
        IFormatter formatter = new BinaryFormatter();
        var target = formatter.Deserialize(stream);
        return target as List<StockPrice>;
    }
}

结果:

Name Serialize(ms) Deserialize(ms) Bytes
BinarySerializer 117 12 242,460

3.2 XML

XML序列化将对象的公共字段和属性或者方法的参数及返回值转换(序列化)为符合特定 XML架构定义语言 (XSD) 文档的 XML 流。由于 XML 是开放式的标准,因此可以根据需要由任何应用程序处理 XML流,而与平台无关。

.NET中执行Xml序列化可以使用XmlSerializer:

public override byte[] Serialize(List<StockPrice> instance)
{
    using (var stream = new MemoryStream())
    {
        var serializer = new System.Xml.Serialization.XmlSerializer(typeof(List<StockPrice>));
        serializer.Serialize(stream, instance);
        return stream.ToArray();
    }
}

public override List<StockPrice> Deserialize(byte[] source)
{
    using (var stream = new MemoryStream(source))
    {
        var serializer = new System.Xml.Serialization.XmlSerializer(typeof(List<StockPrice>));
        var target = serializer.Deserialize(stream);
        return target as List<StockPrice>;
    }
}

结果如下,因为XML格式为了有较好的可读性引入了一些冗余的文本信息,所以体积膨胀了不少:

Name Serialize(ms) Deserialize(ms) Bytes
XmlSerializer 133 26 922,900

3.3 SOAP

XML 序列化还可用于将对象序列化为符合 SOAP 规范的 XML 流。 SOAP 是一种基于 XML 的协议,它是专门为使用 XML 来传输过程调用而设计的,熟悉WCF的应该不会对SOAP感到陌生。

.NET中使用SoapFormatter实现序列化,代码如下:

public override byte[] Serialize(List<StockPrice> instance)
{
    using (var stream = new MemoryStream())
    {
        IFormatter formatter = new SoapFormatter();
        formatter.Serialize(stream, instance.ToArray());
        return stream.ToArray();
    }
}

public override List<StockPrice> Deserialize(byte[] source)
{
    using (var stream = new MemoryStream(source))
    {
        IFormatter formatter = new SoapFormatter();
        var target = formatter.Deserialize(stream);
        return (target as StockPrice[]).ToList();
    }
}

结果如下,由于它本身的特性,体积膨胀得更可怕了(我记得WCF默认就是使用SOAP?):

Name Serialize(ms) Deserialize(ms) Bytes
SoapSerializer 105 123 2,858,416

3.4 JSON

JSON(JavaScript Object Notation)是一种由道格拉斯·克罗克福特构想和设计、轻量级的资料交换语言,该语言以易于让人阅读的文字为基础,用来传输由属性值或者序列性的值组成的数据对象。

虽然.NET提供了DataContractJsonSerializer,但Json.NET更受欢迎,代码如下:

public override byte[] Serialize(List<StockPrice> instance)
{
    using (var stream = new MemoryStream())
    {
        var serializer = new DataContractJsonSerializer(typeof(List<StockPrice>));
        serializer.WriteObject(stream, instance);
        return stream.ToArray();
    }
}

public override List<StockPrice> Deserialize(byte[] source)
{
    using (var stream = new MemoryStream(source))
    {
        var serializer = new DataContractJsonSerializer(typeof(List<StockPrice>));
        var target = serializer.ReadObject(stream);
        return target as List<StockPrice>;
    }
}

结果如下,JSON的体积比XML小很多:

Name Serialize(ms) Deserialize(ms) Bytes
JsonSerializer 40 60 504,320

3.5 Protobuf

其实一开始我和我的同事就清楚用Protobuf最好。

Protocol Buffers 是 Google提供的数据序列化机制。它性能高,压缩效率好,但是为了提高性能,Protobuf采用了二进制格式进行编码,导致可读性较差。

使用protobuf-net需要将序列化的对象使用ProtoContractAttribute和ProtoMemberAttribute进行标记。序列化和反序列化代码如下:

public override byte[] Serialize(List<StockPrice> instance)
{
    using (var stream = new MemoryStream())
    {
        Serializer.Serialize(stream, instance);
        return stream.ToArray();
    }
}

public override List<StockPrice> Deserialize(byte[] source)
{
    using (var stream = new MemoryStream(source))
    {
        return Serializer.Deserialize<List<StockPrice>>(stream);
    }
}

结果十分优秀:

Name Serialize(ms) Deserialize(ms) Bytes
ProtobufSerializer 93 18 211,926

3.6 结果对比

Name Serialize(ms) Deserialize(ms) Bytes
BinarySerializer 117 12 242,460
XmlSerializer 133 26 922,900
SoapSerializer 105 123 2,858,416
JsonSerializer 40 60 504,320
ProtobufSerializer 93 18 211,926

将上述方案的结果列出来对比,Protobuf序列化后体积最少。不过即使是Protobuf,压缩后的数据仍然比文本文档的200K还大,那还不如直接传输这个文本文档。

4. 优化数据结构

其实传输的数据结构上有很大的优化空间。

首先是股票代号Symbol,前面提到获取股价的接口大概是这样:IEnumerable LoadStockPrices(string symbol,DateTime beginDate,DateTime endDate)。既然都知道要获取的股票代号,StockPrice中Symbol这个属性完全就是多余的。

其次是OHLC和PreClosePrice,港股(不记得其它Market是不是这样)的报价肯定是4位有效数字(如95.05和102.4),用float精度也够了,不必用 double。

最后是Date,反正只需要知道日期,不必知道时分秒,直接用与1970-01-01相差的天数作为存储应该就可以了。

private static DateTime _beginDate = new DateTime(1970, 1, 1);

public DateTime Date
{
    get => _beginDate.AddDays(DaysFrom1970);
    set => DaysFrom1970 = (short) Math.Floor((value - _beginDate).TotalDays);
}

[ProtoMember(2)]
[DataMember]
public short DaysFrom1970 { get; set; }

不要以为Volume可以改为int,有些仙股有时会有几十亿的成交量,超过int的最大值2147483647(顺便一提Int32的最大值是2的31次方减1,有时面试会考)。

这样修改后的类结构如下:

[Serializable]
[ProtoContract]
[DataContract]
public class StockPriceSlim
{
    [ProtoMember(1)]
    [DataMember]
    public float ClosePrice { get; set; }

    private static DateTime _beginDate = new DateTime(1970, 1, 1);

    public DateTime Date
    {
        get => _beginDate.AddDays(DaysFrom1970);
        set => DaysFrom1970 = (short) Math.Floor((value - _beginDate).TotalDays);
    }

    [ProtoMember(2)]
    [DataMember]
    public short DaysFrom1970 { get; set; }

    [ProtoMember(3)]
    [DataMember]
    public float HighPrice { get; set; }

    [ProtoMember(4)]
    [DataMember]
    public float LowPrice { get; set; }

    [ProtoMember(5)]
    [DataMember]
    public float OpenPrice { get; set; }

    [ProtoMember(6)]
    [DataMember]
    public float PrvClosePrice { get; set; }

    [ProtoMember(8)]
    [DataMember]
    public double Turnover { get; set; }

    [ProtoMember(9)]
    [DataMember]
    public double Volume { get; set; }
}

序列化的体积大幅减少:

Name Serialize(ms) Deserialize(ms) Bytes
BinarySerializer 11 12 141,930
XmlSerializer 42 24 977,248
SoapSerializer 48 89 2,586,720
JsonSerializer 17 33 411,942
ProtobufSerializer 7 3 130,416

其实之所以有这么大的优化空间,一来是因为传输的对象本身就是ORM生成的对象没针对网络传输做优化,二来各个券商的数据源差不多都是这样传输数据的,最后,本来这个接口是给桌面客户端用的根本就懒得考虑传输数据的大小。

5. 自定义的序列化

由于股票的数据结构相对稳定,而且这个接口不需要通用性,可以自己实现序列化。StockPriceSlim所有属性加起来是38个字节,测试数据是2717条报价,共103246字节,少于Protobuf的130416字节。要达到每个报价只存储38个字节,只需将每个属性的值填入固定的位置:


public override byte[] SerializeSlim(List<StockPriceSlim> instance)
{
    var list = new List<byte>();
    foreach (var item in instance)
    {
        var bytes = BitConverter.GetBytes(item.DaysFrom1970);
        list.AddRange(bytes);

        bytes = BitConverter.GetBytes(item.OpenPrice);
        list.AddRange(bytes);

        bytes = BitConverter.GetBytes(item.HighPrice);
        list.AddRange(bytes);

        bytes = BitConverter.GetBytes(item.LowPrice);
        list.AddRange(bytes);

        bytes = BitConverter.GetBytes(item.ClosePrice);
        list.AddRange(bytes);

        bytes = BitConverter.GetBytes(item.PrvClosePrice);
        list.AddRange(bytes);

        bytes = BitConverter.GetBytes(item.Volume);
        list.AddRange(bytes);

        bytes = BitConverter.GetBytes(item.Turnover);
        list.AddRange(bytes);
    }

    return list.ToArray();
}


public override List<StockPriceSlim> DeserializeSlim(byte[] source)
{
    var result = new List<StockPriceSlim>();
    var index = 0;
    using (var stream = new MemoryStream(source))
    {
        while (index < source.Length)
        {
            var price = new StockPriceSlim();
            var bytes = new byte[sizeof(short)];
            stream.Read(bytes, 0, sizeof(short));
            var days = BitConverter.ToInt16(bytes, 0);
            price.DaysFrom1970 = days;
            index += bytes.Length;

            bytes = new byte[sizeof(float)];
            stream.Read(bytes, 0, sizeof(float));
            var value = BitConverter.ToSingle(bytes, 0);
            price.OpenPrice = value;
            index += bytes.Length;

            stream.Read(bytes, 0, sizeof(float));
            value = BitConverter.ToSingle(bytes, 0);
            price.HighPrice = value;
            index += bytes.Length;

            stream.Read(bytes, 0, sizeof(float));
            value = BitConverter.ToSingle(bytes, 0);
            price.LowPrice = value;
            index += bytes.Length;

            stream.Read(bytes, 0, sizeof(float));
            value = BitConverter.ToSingle(bytes, 0);
            price.ClosePrice = value;
            index += bytes.Length;

            stream.Read(bytes, 0, sizeof(float));
            value = BitConverter.ToSingle(bytes, 0);
            price.PrvClosePrice = value;
            index += bytes.Length;

            bytes = new byte[sizeof(double)];
            stream.Read(bytes, 0, sizeof(double));
            var volume = BitConverter.ToDouble(bytes, 0);
            price.Volume = volume;
            index += bytes.Length;

            bytes = new byte[sizeof(double)];
            stream.Read(bytes, 0, sizeof(double));
            var turnover = BitConverter.ToDouble(bytes, 0);
            price.Turnover = turnover;
            index += bytes.Length;

            result.Add(price);
        }
        return result;
    }
}

结果如下:

Name Serialize(ms) Deserialize(ms) Bytes
CustomSerializer 5 1 103,246

这种方式不仅序列化后的体积最小,而且序列化和反序列化的速度都十分优秀,不过代码十分难看而且没有扩展性。尝试用反射改进一下:

public override byte[] SerializeSlim(List<StockPriceSlim> instance)
{
    var result = new List<byte>();
    foreach (var item in instance)
        foreach (var property in typeof(StockPriceSlim).GetProperties())
        {
            if (property.GetCustomAttribute(typeof(DataMemberAttribute)) == null)
                continue;

            var value = property.GetValue(item);
            byte[] bytes = null;
            if (property.PropertyType == typeof(int))
                bytes = BitConverter.GetBytes((int)value);
            else if (property.PropertyType == typeof(short))
                bytes = BitConverter.GetBytes((short)value);
            else if (property.PropertyType == typeof(float))
                bytes = BitConverter.GetBytes((float)value);
            else if (property.PropertyType == typeof(double))
                bytes = BitConverter.GetBytes((double)value);
            result.AddRange(bytes);
        }

    return result.ToArray();
}

public override List<StockPriceSlim> DeserializeSlim(byte[] source)
{
    using (var stream = new MemoryStream(source))
    {
        var result = new List<StockPriceSlim>();
        var index = 0;

        while (index < source.Length)
        {
            var price = new StockPriceSlim();
            foreach (var property in typeof(StockPriceSlim).GetProperties())
            {
                if (property.GetCustomAttribute(typeof(DataMemberAttribute)) == null)
                    continue;

                byte[] bytes = null;
                object value = null;

                if (property.PropertyType == typeof(int))
                {
                    bytes = new byte[sizeof(int)];
                    stream.Read(bytes, 0, bytes.Length);
                    value = BitConverter.ToInt32(bytes, 0);
                }
                else if (property.PropertyType == typeof(short))
                {
                    bytes = new byte[sizeof(short)];
                    stream.Read(bytes, 0, bytes.Length);
                    value = BitConverter.ToInt16(bytes, 0);
                }
                else if (property.PropertyType == typeof(float))
                {
                    bytes = new byte[sizeof(float)];
                    stream.Read(bytes, 0, bytes.Length);
                    value = BitConverter.ToSingle(bytes, 0);
                }
                else if (property.PropertyType == typeof(double))
                {
                    bytes = new byte[sizeof(double)];
                    stream.Read(bytes, 0, bytes.Length);
                    value = BitConverter.ToDouble(bytes, 0);
                }

                property.SetValue(price, value);
                index += bytes.Length;
            }


            result.Add(price);
        }
        return result;
    }
}
Name Serialize(ms) Deserialize(ms) Bytes
ReflectionSerializer 413 431 103,246

好像好了一些,但性能大幅下降。我好像记得有人说过.NET会将反射缓存让我不必担心反射带来的性能问题,看来我的理解有出入。索性自己缓存些反射结果:

private readonly IEnumerable<PropertyInfo> _properties;

public ExtendReflectionSerializer()
{
    _properties = typeof(StockPriceSlim).GetProperties().Where(p => p.GetCustomAttribute(typeof(DataMemberAttribute)) != null).ToList();
}
Name Serialize(ms) Deserialize(ms) Bytes
ExtendReflectionSerializer 11 11 103,246

这样改进后性能还可以接受。

6. 最后试试压缩

最后试试在序列化的基础上再随便压缩一下:

public byte[] SerializeWithZip(List<StockPriceSlim> instance)
{
    var bytes = SerializeSlim(instance);

    using (var memoryStream = new MemoryStream())
    {
        using (var deflateStream = new DeflateStream(memoryStream, CompressionLevel.Fastest))
        {
            deflateStream.Write(bytes, 0, bytes.Length);
        }
        return memoryStream.ToArray();
    }
}

public List<StockPriceSlim> DeserializeWithZip(byte[] source)
{
    using (var originalFileStream = new MemoryStream(source))
    {
        using (var memoryStream = new MemoryStream())
        {
            using (var decompressionStream = new DeflateStream(originalFileStream, CompressionMode.Decompress))
            {
                decompressionStream.CopyTo(memoryStream);
            }
            var bytes = memoryStream.ToArray();
            return DeserializeSlim(bytes);
        }
    }
}

结果看来不错:

Name Serialize(ms) Deserialize(ms) Bytes Serialize With Zip(ms) Deserialize With Zip(ms) Bytes With Zip
BinarySerializer 11 12 141,930 22 12 72,954
XmlSerializer 42 24 977,248 24 28 108,839
SoapSerializer 48 89 2,586,720 61 87 140,391
JsonSerializer 17 33 411,942 24 35 90,125
ProtobufSerializer 7 3 130,416 7 6 65,644
CustomSerializer 5 1 103,246 9 3 57,697
ReflectionSerializer 413 431 103,246 401 376 59,285
ExtendReflectionSerializer 11 11 103,246 13 14 59,285

7. 结语

满足了好奇心,顺便复习了一下各种序列化的方式。

因为原来的需求就很单一,没有测试各种数据量下的对比。

虽然Protobuf十分优秀,但在本地存储序列化文件时为了可读性我通常都会选择XML或JSON。

8. 参考

二进制序列化
XML 和 SOAP 序列化
Json.NET
Protocol Buffers - Google's data interchange format

9. 源码

StockDataSample

目录
相关文章
|
3天前
|
机器学习/深度学习 人工智能 Cloud Native
洞察.NET 技术的前沿应用
【7月更文挑战第4天】**洞察.NET技术前沿:.NET Core跨平台崛起,云原生与AI应用深化。ML.NET、TensorFlow.NET助力机器学习,Xamarin与MAUI统一跨平台UI。Azure云服务支持下,.NET引领软件开发新趋势。**
19 5
|
3天前
|
人工智能 物联网 开发工具
.NET技术:多元语言、丰富库与跨平台能力引领软件开发新纪元。
`【7月更文挑战第4天】.NET技术:多元语言、丰富库与跨平台能力引领软件开发新纪元。从企业应用、云服务到游戏开发,其角色日益凸显。随着微软的持续创新与社区合作,未来.NET将在物联网、AI等领域拓宽应用,开发者应把握趋势,共创未来。`
8 0
|
3天前
|
人工智能 开发框架 Devops
.NET技术概览:** 本文探讨了.NET的核心特性,包括多语言支持、Common Language Runtime、丰富的类库和跨平台能力,强调其在企业级、Web、移动及游戏开发中的应用。
【7月更文挑战第4天】.NET技术概览:** 本文探讨了.NET的核心特性,包括多语言支持、Common Language Runtime、丰富的类库和跨平台能力,强调其在企业级、Web、移动及游戏开发中的应用。此外,讨论了.NET如何通过性能优化、DevOps集成、AI与ML支持以及开源策略应对未来挑战,为开发者提供强大工具,共创软件开发新篇章。
12 3
|
3天前
|
人工智能 前端开发 开发工具
**.NET技术概览:** 本文探讨.NET的核心优势
【7月更文挑战第4天】**.NET技术概览:** 本文探讨了.NET的核心优势,如统一开发平台、Visual Studio的强大工具、跨平台能力及丰富的类库。它在现代应用中的创新应用包括企业级、Web、移动、云服务和游戏开发。同时,面对性能优化、容器化、AI集成等挑战,.NET正寻求未来机遇,通过开源社区持续发展。开发者应抓住这些趋势,利用.NET推动软件创新。
12 1
|
3天前
|
人工智能 前端开发 Devops
NET技术在现代开发中的影响力日益增强,本文聚焦其核心价值,如多语言支持、强大的Visual Studio工具、丰富的类库和跨平台能力。
【7月更文挑战第4天】**.NET技术在现代开发中的影响力日益增强,本文聚焦其核心价值,如多语言支持、强大的Visual Studio工具、丰富的类库和跨平台能力。实际应用涵盖企业系统、Web、移动和游戏开发,以及云服务。面对性能挑战、容器化、AI集成及跨平台竞争,.NET持续创新,开发者应关注技术趋势,提升技能,并参与社区,共同推进技术发展。**
8 1
|
3天前
|
机器学习/深度学习 人工智能 开发者
.NET 技术:为开发带来新机遇
【7月更文挑战第4天】**.NET技术开启软件开发新篇章,通过跨平台革命(.NET Core, Xamarin, .NET MAUI)、云服务与微服务(Azure, DevOps, Docker)及AI集成(ML.NET, 认知服务, TensorFlow)为开发者创造新机遇。开源社区的繁荣与性能提升使.NET更具竞争力,推动智能应用的创新与发展。开发者需紧跟潮流,利用这些工具和框架构建高效、创新的解决方案。**
6 1
|
29天前
|
Cloud Native API C#
C#的现代化:.NET Core引领的技术革命
【6月更文挑战第9天】`.NET Core引领C#现代化,实现跨平台革命,提升性能并支持云原生应用。异步编程模型优化体验,统一API简化开发流程。C#应用场景扩展,开发效率提高,技术创新加速,预示其未来在技术领域将持续发挥关键作用。`
30 10
|
3天前
|
人工智能 前端开发 开发工具
.NET技术探析:优势、创新应用及挑战。
【7月更文挑战第4天】**.NET技术探析:优势、创新应用及挑战。本文分三部分展开,阐述了.NET作为统一多语言开发平台的核心优势,如强大的Visual Studio工具、跨平台能力与丰富的类库;探讨了其在企业级、Web、移动及游戏开发中的创新角色;并指出面临性能优化、容器化、AI集成等挑战及未来开源社区驱动的发展机遇。通过理解与应对,开发者可借助.NET推动软件开发进步。**
11 0
|
10天前
|
XML 机器学习/深度学习 移动开发
技术笔记:log4net使用详解
技术笔记:log4net使用详解
15 0
|
10天前
|
安全 Java 开发者
技术心得:反序列化Gadget学习篇五CommonCollections3
技术心得:反序列化Gadget学习篇五CommonCollections3