[.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而已。
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. 源码
[.NET]使用十年股价对比各种序列化技术的更多相关文章
- 微信小程序与传统APP十大优劣对比
随着微信公众平台的开放,微信端小程序涌现市场,带来很很多便利和简单的原生操作,询:微信端小程序是否会替代传统的APP应用?两者的优劣如何?我们一起来看看传统APP与微信端小程序十大优劣对比 ...
- 重温CLR(十八) 运行时序列化
序列化是将对象或对象图转换成字节流的过程,反序列化是将字节流转换回对象图的过程.在对象和字节流之间转换是很有用的机制. 1 应用程序的状态(对象图)可轻松保存到磁盘文件或数据库中,并在应用程序下次运行 ...
- .net中对象序列化技术浅谈
.net中对象序列化技术浅谈 2009-03-11 阅读2756评论2 序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储 ...
- Java序列化技术与Protobuff
http://www.cnblogs.com/fangfan/p/4094175.html http://www.cnblogs.com/fangfan/p/4094175.html 前言: Java ...
- Java序列化技术即将被废除!!!
我们的对象并不只是存在内存中,还需要传输网络,或者保存起来下次再加载出来用,所以需要Java序列化技术.Java序列化技术正是将对象转变成一串由二进制字节组成的数组,可以通过将二进制数据保存到磁盘或者 ...
- 基于序列化技术(Protobuf)的socket文件传输
好像好久都没更博文了,没办法,最近各种倒霉事情,搞到最近真的没什么心情,希望之后能够转运吧. 言归正传,这次我要做的是基于序列化技术的socket文件传输来无聊练一下手. 一.socket文件传输 之 ...
- 揭开DRF序列化技术的神秘面纱
在RESTful API中,接口返回的是JSON,JSON的内容对应的是数据库中的数据,DRF是通过序列化(Serialization)的技术,把数据模型转换为JSON的,反之,叫做反序列化(dese ...
- Protocol Buffer序列化对比Java序列化.
初识 Protocol Buff是谷歌推出的一种序列化协议. 而Java序列化协议也是一种协议. 两者的目的是, 将对象序列化成字节数组, 或者说是二进制数据, 那么他们之间有什么差异呢. proto ...
- YbSoftwareFactory 代码生成插件【二十】:DynamicObject的序列化
DynamicObject 是 .NET 4.0以来才支持的一个类,但该类在.NET 4.0下未被标记为[Serializable] Attribute,而在.NET 4.5下则被标记了[Serial ...
随机推荐
- 【技术干货】git常用命令
2.1 git init语法: git init在当前目录初始化git仓库,适用于尚未使用git管理的项目2.2 git clone语法: git clone <url>例如: git c ...
- 某控股公司OA系统ORACLE DG搭建
*此处安装ORACLE DATAGUARD是利用ORACLE RMAN DUPLICATE方式安装.*可以搭建好ORACLE DG再来impdp生产数据,也可以先导入主库数据再来做DG*注意看下面的配 ...
- 学习资料分享:Python能做什么?
最近一直忙着研究学习Python,很久没更新博客了,整理了一些Python学习资料,和大家分享一下!每天更新一篇~ 一.Python 特点 1.易于学习:Python有相对较少的关键字,结构简单,和一 ...
- Selenium常用API用法示例集----下拉框、文本域及富文本框、弹窗、JS、frame、文件上传和下载
元素识别方法.一组元素定位.鼠标操作.多窗口处理.下拉框.文本域及富文本框.弹窗.JS.frame.文件上传和下载 元素识别方法: driver.find_element_by_id() driver ...
- c的文件流读取
strtok(数组,分隔符); atof(数组)返回值为转换后的数字; fgets(数组指针,长度,文件句柄); 整整花了两天啊
- leetcode第一天
leetcode 第一天 2017年12月24日 第一次刷leetcode真的是好慢啊,三道题用了三个小时,而且都是简单题. 数组 1.(674)Longest Continuous Increasi ...
- 《android开发艺术探索》读书笔记(十五)--Android性能优化
接上篇<android开发艺术探索>读书笔记(十四)--JNI和NDK编程 No1: 如果<include>制定了这个id属性,同时被包含的布局文件的根元素也制定了id属性,那 ...
- nyoj61 传纸条(一) dp
思路:两人一个从左上角出发只能向右和向下,另一人从右下角出发只能向左和向上,可以看做两人都是从右下角出发,且只能向左和向上传纸条,并且两条路径不会相交,因为一个人只会传一次,那么随便画一个图就能知道两 ...
- 简单的GIT上传
简单的GIT上传 上传项目时先新建一个 文件夹 mkdir test 然后在切换到test文件夹中然后把github 中的项目拷贝下来 git glone url 然后git init 查看文件 然后 ...
- Linux CentOS安装配置MySQL数据库
没什么好说的,直接正面刚吧. 安装mysql数据库 a)下载mysql源安装包:wget http://dev.mysql.com/get/mysql57-community-release-el7- ...