[翻译] 编写高性能 .NET 代码--第五章 通用编码与对象设计 -- 类 vs 结构体
本章介绍了本书其它部分未涉及到的一些编码和设计原则。包含了一些.NET的应用场景,有些不会造成太大危害,有些则会造成明显的问题。剩下的则根据你的使用方法会产生不同的效果。如果要对本章节出现的原则做一个总结,那就是:
过度的优化会影响代码的抽象
这意味着,当你希望更高的优化性能,你需要了解每个层次代码的实现细节。本章会有很多相关介绍。
类 vs 结构体
类的实例都是在堆上分配的,通过指针的引用进行访问。传递这些对象代价很低,因为它只是一个指针(4或者8直接)的拷贝。然而,对象也有一些固定开销:8或16字节(32或64位系统)。这些开销包括指向方法表的指针和用于其它目的同步字段。但是,如果通过调试工具查看一个空对象占用的内存,这会发现大了13或者24字节(32位或64位系统)。这是.NET的内存对齐机制导致的。
而结构体则没上面的开销,它的内存使用量就是字段大小的综合。如果结构体是方法(函数)里声明的局部变量,则它在堆栈上分配控件。如果结构体被声明为类的一部分,这结构体使用的内存这是该类的内存布局里的一部分(因此它会分配在堆上)。但你将结构体传递给方法(函数)时,他将对字节数据做复制。因为它不在堆上,结构体是不会导致垃圾回收的。
因此这里有一个折中。你可以找到各种关于结构体尺寸大小的建议,但这里我不会告诉你一个确切的数字。在大多数情况下,你结构体需要保持一个比较小的尺寸,特别是他们需要经常被传递,你需要保证结构体的大小不会造成太大的问题。唯一能确定的是,你需要根据自己的应用场景进行分析。
有些情况下,效率的差别还是蛮大的。当一个对象开销看起来不是很多,但是对比一个对象数组和结构体数组就可以看出差别。在32位系统下,假设一个数据结构包含16字节的数据,数组长度是100w。
使用对象数组占用的空间
8字节数组开销+
(4字节指针地址X1,000,000)+
((8字节头部+16字节数据)X1,000,000)
=28MB
使用结构体数组占用的空间
8字节数组开销+
(16字节数据X1,000,100)
=16MB
如果使用64位系统,对象数组则使用40MB,而结构体数组仍然是16MB。
可以看到,在一个结构数组中,相同大小的数据占用的内存小。随着对象数组里对象的增加,还会增加GC的压力。
除了空间,还有CPU效率问题。CPU有多级缓存。越靠近CPU的缓存越小,但访问速度也会更快,对于顺序保存的数据越容易优化。
对于一个结构体数组,他们在内存里都是连续的值。访问结构体数组里数据很简单,只要找到正确的位置就可以得到对应的值。这就意味着在大数组数据做迭代访问有巨大的差异。如果该值已经在CPU的告诉缓存中,它的访问速度是要比访问RAM要快一个数量级。
如果要访问对象数组里的某一项,需要先获得该对象的指针引用,再去堆里访问。迭代对象数组的时候,就会造成数据指针在堆里跳转,频繁更新CPU的缓存,进而浪费了很多访问CPU缓存数据机会。
在很多时候,通过改进数据保存在内存的位置,降低CPU访问内存的开销是使用结构体的一个主要原因,它可以显著的提升性能。
因为结构体使用的时候总是被复制,所以编码时要很小心,否则你会产生一些有趣的bug。例如下面的栗子,你是无法通过编译的:
struct Point
{
public int x;
public int y;
}
public static void Main()
{
List<Point> points = new List<Point>();
points.Add(new Point() {x = 1, y = 2});
points[0].x = 3;
}
问题是在最后一行,你试图修改列表里Point元素的某个值,这个操作是不行的,因为points[0]返回的是原始值的一个副本。正确的修改值的方式是
Point p = points[0];
p.x = 3;
points[0] = p;
但是,你可以采取更严格的编码策略:不要修改结构体。一旦结构体创建,永远不要改变他的值。这可以消除了上面的编译问题,并简化了结构体的使用规则。
我之前提到,结构体应该保持小的体积,已避免花费大量的时间来复制他们,但是偶尔也会使用一些大的结构体。例如一个最终商业流程细节的对象,里面需要保存大量的时间戳:
class Order
{
public DateTime ReceivedTime { get; set; }
public DateTime AcknowledgeTime { get; set; }
public DateTime ProcessBeginTime { get; set; }
public DateTime WarehouseReceiveTime { get; set; }
public DateTime WarehouseRunnerReceiveTime { get; set; }
public DateTime WarehouseRunnerCompletionTime { get; set; }
public DateTime PackingBeginTime { get; set; }
public DateTime PackingEndTime { get; set; }
public DateTime LabelPrintTime { get; set; }
public DateTime CarrierNotifyTime { get; set; }
public DateTime ProcessEndTime { get; set; }
public DateTime EmailSentToCustomerTime { get; set; }
public DateTime CarrerPickupTime { get; set; }
// lots of other data ...
}
为了简化代码,我们可以将时间的数据划分到自己的子结构里,这样我们可以通过这样的方式访问Order对象:
Order order = new Order();
Order.Times.ReceivedTime = DateTime.UtcNow;
我们可以把数据全部放到自己的类里:
class OrderTimes
{
public DateTime ReceivedTime { get; set; }
public DateTime AcknowledgeTime { get; set; }
public DateTime ProcessBeginTime { get; set; }
public DateTime WarehouseReceiveTime { get; set; }
public DateTime WarehouseRunnerReceiveTime { get; set; }
public DateTime WarehouseRunnerCompletionTime { get; set; }
public DateTime PackingBeginTime { get; set; }
public DateTime PackingEndTime { get; set; }
public DateTime LabelPrintTime { get; set; }
public DateTime CarrierNotifyTime { get; set; }
public DateTime ProcessEndTime { get; set; }
public DateTime EmailSentToCustomerTime { get; set; }
public DateTime CarrerPickupTime { get; set; }
}
class Order
{
public OrderTimes Times;
}
但是,这样会为每个Order对象引入额外的12或者24字节的开销。如果你需要将OrderTimes对象作为一个整体传入各种方法函数里,这也许是有一定道理的,但为什么不把Order对象传入方法里呢?如果你同时有数千个Order对象,则可能会导致更多的垃圾回收,这是额外的对象增加的引用导致的。
相反,将OrderTime更改为结构体,通过Order上的属性(例如:Order.Times.ReceivedTime)访问OrderTImes结构体的各个属性,不会导致结构体的副本(.NET会对这个访问做优化)。这样OrderTimes结构体基本上成为Order类的内存布局的一部分,几乎和没有子结构体一样了,你拥有了更加漂亮的代码。
这种技术确实违反了不可变的结构体原理,但这里的技巧就是将OrderTimes结构的字段视为Order对象的字段。你不需要将OrderTimes结构体作为一个实体进行传递,它只是一个代码组织方式。
[翻译] 编写高性能 .NET 代码--第五章 通用编码与对象设计 -- 类 vs 结构体的更多相关文章
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 减少分配率, 最重要的规则,缩短对象的生命周期,减少对象层次的深度,减少对象之间的引用,避免钉住对象(Pinning)
减少分配率 这个几乎不用解释,减少了内存的使用量,自然就减少GC回收时的压力,同时降低了内存碎片与CPU的使用量.你可以用一些方法来达到这一目的,但它可能会与其它设计相冲突. 你需要在设计对象时仔细检 ...
- [翻译]编写高性能 .NET 代码 第一章:工具介绍 -- Visual Studio
<<返回目录 Visual Studio vs虽然不是全宇宙唯一的IDE,但它是.net开发人员最常用的开发工具.它自带一个性能分析工具,你可以使用它来做开发,不同的vs版本在工具上会略有 ...
- [翻译]编写高性能 .NET 代码 第一章:工具介绍 -- Performance Counters(性能计数器)
<<返回目录 Performance Counters(性能计数器) 性能计数器是监视应用程序和系统性能的最简单的方法之一.它有几十个类别数百个计数器在,包括一些.net特有的计数器.要访 ...
- [翻译]编写高性能 .NET 代码 第二章:垃圾回收
返回目录 第二章:垃圾回收 垃圾回收是你开发工作中要了解的最重要的事情.它是造成性能问题里最显著的原因,但只要你保持持续的关注(代码审查,监控数据)就可以很快修复这些问题.我这里说的"显著的 ...
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 避免使用终结器,避免大对象,避免复制缓冲区
避免使用终结器 如果没有必要,是不需要实现一个终结器(Finalizer).终结器的代码主要是让GC回收非托管资源用.它会在GC完成标记对象为可回收后,放入一个终结器队列里,在由另外一个线程执行队列里 ...
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 将长生命周期对象和大对象池化
将长生命周期对象和大对象池化 请记住最开始说的原则:对象要么立即回收要么一直存在.它们要么在0代被回收,要么在2代里一直存在.有些对象本质是静态的,生命周期从它们被创建开始,到程序停止才会结束.其它对 ...
- [翻译]编写高性能 .NET 代码 第一章:性能测试与工具 -- 平均值 vs 百分比
<<返回目录 平均值 vs 百分比 在考虑要性能测试的目标值时,我们需要考虑用什么统计口径.大多数人都会首选平均值,但在大多数情况下,这个正确的,但你也应该适当的考虑百分数.但你有可用性的 ...
- [翻译]编写高性能 .NET 代码 第二章:垃圾回收 基本操作
返回目录 基本操作 垃圾回收的算法细节还在不断完善中,性能还会有进一步的提升.下文介绍的内容在不同的.NET版本里会略有不同,但大方向是不会有变动的. 在.net进程里会管理2个类型的内存堆:托管和非 ...
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 配置选项
配置选项 在基于"less rope to hang yourself with"思想下,.NET 框架没有给开发提供很多太多的配置选项.但在大多数情况下,GC会跟你的硬件配置,及 ...
随机推荐
- Sencha+cordova 构造 华丽手机程序,并讲讲,在商用项目中经常用到的cordova插件(一)
采用 Sencha Architect 3 ,编辑设计,我只想说一个结论,jq能搞的 Sencha touch也能做到(只是 设计思路 更面向对象,学习知识量大点而已 ,不理解 MVC 就不用选了), ...
- jquery分页插件的修改
前言 最近分页功能使用的比较多,所以从网上下载个jquery分页插件来使用, 之前用的都挺好的,直到昨天出现了逻辑问题,反复查看自己的代码,最后发现是点击页码后执行了多个点击事件.最后只有自己查看源码 ...
- tomcat的常用配置
1.解決get请求的中文乱码问题 解决办法: 首先找到tomcat路径下的apache-tomcat-7.0.52\conf文件夹,打开server.xml文件,编辑如下内容: <Connect ...
- 微信公众号、H5、APP三者各有什么优势?
昨天给大家分享了一个现在很热的H5,众所周知,当下H5手机网站.微信公众号.APP这三种载体都越来越火了,而且三者都有各自的一些优势和劣势. HTML5(H5) H5之所以能引发如此广泛的效应,根本在 ...
- 子集构造法实现NFA的确定化
功能: 采用子集构造算法实现NFA的确定化 输入:读取NFA的文件(文件名test.txt), 文件格式: 第一列表示状态名,第二列和第三列分别表示输入字符a和b到达的状态 输出:确定化后的DFA( ...
- ThinkPHP 框架模型
1 在MainController.class.php 控制器中有一个test的方法,同时还有一个deng的方法,我想在test方法中使用deng方法 表示为 <?php namespace ...
- oracle AWR性能监控报告生成方法
目前相当一部分公司会用到oracle,在做性能测试的时候,对数据库的监控很重要,那么这里先介绍下如何生成oracle自带的awr监控报告,而具体报告的内容分析会放在后续的博客中 oracle性能分析入 ...
- 【Objective-C 基础】4.分类和协议
1.分类 OC提供了一种与众不同的方式--Category,可以动态的为已经存在的类添加新的行为(方法) 这样可以保证类的原始设计规模较小,功能增加时再逐步扩展. 使用Category对类进行扩展时, ...
- ios随机数不重复
NSMutableArray* Valuearr=[NSMutableArray new]; int counN=9; for(int i=0;i<counN;i++) //创 ...
- Git相关操作及记录
一.软件 1.下载Git客户端软件 Widows平台: https://github.com/git-for-windows/git/releases/download/v2.13.0.windows ...