.NET陷阱之六:从枚举值持久化带来大量空间消耗谈起
好长时间没有写博文了,今天继续。
这次跟大家分享的内容起因于对一个枚举值列表的序列化,下面简化后的代码即能重现。为了明确起见,我显式指定了枚举的基础类型。
// 定义一个枚举类型。
public enum SomeEnum :int
{
First,
Second,
Third,
... ...
} // 重现问题的代码。
var list = new List<SomeEnum>();
for (int i = ; i < ; ++i)
{
list.Add((SomeEnum)(i % ));
} var formatter = new BinaryFormatter();
var stream = File.OpenWrite("c:\\a.data");
formatter.Serialize(stream, list);
stream.Close()
你预料生成的a.data文件大约有多大?
- 如果你估计的结果是12K以上,那么你应该知道我要说什么了,可以洗洗睡了;
- 如果你估计的结果是4K多一些,那么请继续看本文后面的内容。
得到4K结果的同学,我想是这样估计的,SomeEnum枚举用int表示,每个值占用4字节,1000个大约就是4K左右,加上其它一些序列化信息,可能就4K多一些吧。最初我也是这么想的,直到在软件中这样的列表占用了几十兆的内存时,问题才暴露出来。我想我还是比较天真,以为那么简洁的类型应该有相应简洁的序列化方式,我甚至天真到从来没有意识到这是个问题。
我用Reflector跟踪了具体的持久化过程,才发现原来在.NET framework内部,对枚举值并没有像基本类型那样进行处理,而是直接当成普通的值对象处理的。更糟糕的是,对于值对象的处理,居然也要像引用对象那样保存objectId和mapId。我用了“居然”这个词,因为我真的认为值对象(ValueType)就只是数据,不会存在两个reference引用同一个值对象的情况(我知道这样说有些奇怪,但希望你能明白我的意思)——直到现在我也这么认为。
下面是 formatter.Serialize(stream, list) 这句代码执行过程中某一时刻的堆栈状态,为了避免大量的折行影响你的心情,我只保留了函数名部分。
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.BinaryObject.Write(...)
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.__BinaryWriter.WriteObject(...)
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(...)
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(...)
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteArrayMember(...)
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteArray(...)
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(...)
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(...)
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(...)
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(System.IO.Stream serializationStream, object graph)
在栈顶上是.NET framework二进制序列化中BinaryObject.Write方法,其实现如下:
public void Write(__BinaryWriter sout)
{
sout.WriteByte();
sout.WriteInt32(this.objectId);
sout.WriteInt32(this.mapId);
}
也就是说每写一个枚举值,系统都会先写入1 + 4 + 4 = 9个字节的额外数据!这样算起来,开始处代码产生的文件就大约是 1K * (9 + 4) = 13K !
这几天我一直在想:为什么对值对象也要写入objectId和mapId呢?根据框架的代码的实际输出来看,系统不会“对值相等的多个值对象只保存一份数据”,那么为什么还要写入这些额外的数据呢?对此我仍不得其解,如果有人知道,还请不吝赐教。
为了解决这个问题,我在类型内部使用了List<int>来保存数据,而在对外接口中完成int和SomeEnum的转换,这样做一来不会影响其它模块的代码,二来也可以将此处理进行屏蔽。
基于同样的原因,对于如下一个值类型来说,要直接使用.NET提供的序列化机制,则每保存一个对象,将额外消耗一倍多的空间。是的,对于引用类型来说也是一样,但还是那句话——我只是没有意识到这个问题,或者说现在还不能接受framework那么粗糙的实现!
[Serializable]
public struct Point
{
private float x, y;
}
为了避免这样的问题,最直接的方法是在包含此类成员的类型上实现ISerializable接口,然后存储转换到基本类型的数据。如果类中要序列化的成员比较多的话,这样做可能会导致其它成员也要手工处理。如果感兴趣,也可以参考我的另一篇博文《深入挖掘.NET序列化机制——实现更易用的序列化方案》看看能不能实现一个统一的机制。
最后再次呼吁:有谁能告诉我微软为什么要如此处理值类型的序列化?
.NET陷阱之六:从枚举值持久化带来大量空间消耗谈起的更多相关文章
- AX2012 multiple enum values as query filter选择多个枚举值当过滤条件
classDeclaration { QueryBuildRange qbrLocationType; } datasource.init() { super(); qbrLocationType = ...
- 获取枚举值上的Description特性说明
/// <summary> /// 获取枚举值上的Description特性说明 /// </summary> /// <typeparam name="T&q ...
- C#八皇后问题 枚举值
记得刚出道的时候, 有考虑怎么面试, 以及可能会遇到的面试题, 有一个人说了一下 八皇后问题, 据说要用 sql 语句写出来, 暂时我 写了一个C#版本的, 经测验,八皇后算法结果为 92种, 这个与 ...
- MVC3不能正确识别JSON中的Enum枚举值
一.背景 在MVC3项目里,如果Action的参数中有Enum枚举作为对象属性的话,使用POST方法提交过来的JSON数据中的枚举值却无法正确被识别对应的枚举值. 二.Demo演示 为了说明问题,我使 ...
- 在C#中如何读取枚举值的描述属性
在C#中,有时候我们需要读取枚举值的描述属性,也就是说这个枚举值代表了什么意思.比如本文中枚举值 Chinese ,我们希望知道它代表意思的说明(即“中文”). 有下面的枚举: 1 2 3 4 5 6 ...
- 利用DescriptionAttribute定义枚举值的描述信息 z
System.ComponentModel命名空间下有个名为DescriptionAttribute的类用于指定属性或事件的说明,我所调用的枚举值描述信息就是DescriptionAttribute类 ...
- C# .NET 获取枚举值的自定义属性(特性/注释/备注)信息
一.引言 枚举为我看日常开发的可读性提供的非常好的支持,但是有时我们需要得到枚举值得描述信息或者是注释(备注)信息 比如要获得 TestEmun.aaa 属性值得备注 AAA,比较不方便得到. pub ...
- 对WM_NCHITTEST消息的了解+代码实例进行演示(消息产生消息,共24个枚举值)
这个消息比较实用也很关键,它代表非显示区域命中测试.这个消息优先于所有其他的显示区域和非显示区域鼠标消息.其中lParam参数含有鼠标位置的x和y屏幕坐标,wParam 这里没有用. Windows应 ...
- 获取Enum枚举值描述的几法方法
原文:获取Enum枚举值描述的几法方法 1.定义枚举时直接用中文 由于VS对中文支持的很不错,所以很多程序员都采用了此方案. 缺点:1.不适合多语言 2.感觉不太完美,毕竟大部分程序员大部分代码都使用 ...
随机推荐
- jquery函数理解与运用
javascript中有多种不用的方式去理解函数,函数类似于我们以前学过的数学函数,但是在程序设计中,我是按照下面的方式进行理解: 函数的理解: 函数是一个代码块,内容被包含在函数内,通常我们是把一些 ...
- Delphi 关键字详解[整理于 "橙子" 的帖子]
absolute //它使得你能够创建一个新变量, 并且该变量的起始地址与另一个变量相同. var Str: ]; StrLen: Byte absolute Str; //这个声明指定了变量 ...
- iOS Hit-Test应用
最近又看了遍苹果的官方文档<Event Handling Guide for iOS>,对事件响应链中的hit-test view 又多了些理解,个人觉的官方文档对这块讲的非常简单,很多东 ...
- 简易版C语言程序语法
<程序> -〉 <外部声明> | <函数定义><外部声明> -〉<头文件> | <变量> | <结构体> <头 ...
- XVI Open Cup named after E.V. Pankratiev. GP of Ukraine
A. Associated Vertices 首先求出SCC然后缩点,第一次求出每个点能到的点集,第二次收集这些点集即可,用bitset加速,时间复杂度$O(\frac{nm}{64})$. #inc ...
- 总结30个CSS3选择器(转载)
或许大家平时总是在用的选择器都是:#id .class 以及标签选择器.可是这些还远远不够,为了在开发中更加得心应手,本文总结了30个CSS3选择器,希望对大家有所帮助. 1 *:通用选择器 * ...
- CodeSimth-.NetFrameworkDataProvider可能没有安装。解决方法
原文地址:http://www.haogongju.net/art/2561889 1.下载System.Data.SQLite驱动:注意:根据自己的CPU选择是32位还是64位的驱动.建议选择4.0 ...
- overflow:hidden清除浮动原理
overflow:hidden的意思是超出部分去掉,如果父元素height为auto,内部元素浮动,势必会将内部元素全部隐藏,故计算出内部浮动高度顺便清除浮动.
- JAVA面试逻辑题1
一.计算推理 烧香问题: 有两根不均匀分布的香,每一根烧完的时间都是一小时.用什么办法确定一段15分钟的时间? 解题步骤: 1.点燃第一根的两头,同时点燃第二根的一头: 2.等到第一根燃尽以后,再点燃 ...
- dubbo 常见错误
1. Caused by: java.lang.reflect.MalformedParameterizedTypeException 或 Caused by: java.lang.NoSuchMet ...