返回目录

本章介绍了本书其它部分未涉及到的一些编码和设计原则。包含了一些.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 结构体的更多相关文章

  1. [翻译] 编写高性能 .NET 代码--第二章 GC -- 减少分配率, 最重要的规则,缩短对象的生命周期,减少对象层次的深度,减少对象之间的引用,避免钉住对象(Pinning)

    减少分配率 这个几乎不用解释,减少了内存的使用量,自然就减少GC回收时的压力,同时降低了内存碎片与CPU的使用量.你可以用一些方法来达到这一目的,但它可能会与其它设计相冲突. 你需要在设计对象时仔细检 ...

  2. [翻译]编写高性能 .NET 代码 第一章:工具介绍 -- Visual Studio

    <<返回目录 Visual Studio vs虽然不是全宇宙唯一的IDE,但它是.net开发人员最常用的开发工具.它自带一个性能分析工具,你可以使用它来做开发,不同的vs版本在工具上会略有 ...

  3. [翻译]编写高性能 .NET 代码 第一章:工具介绍 -- Performance Counters(性能计数器)

    <<返回目录 Performance Counters(性能计数器) 性能计数器是监视应用程序和系统性能的最简单的方法之一.它有几十个类别数百个计数器在,包括一些.net特有的计数器.要访 ...

  4. [翻译]编写高性能 .NET 代码 第二章:垃圾回收

    返回目录 第二章:垃圾回收 垃圾回收是你开发工作中要了解的最重要的事情.它是造成性能问题里最显著的原因,但只要你保持持续的关注(代码审查,监控数据)就可以很快修复这些问题.我这里说的"显著的 ...

  5. [翻译] 编写高性能 .NET 代码--第二章 GC -- 避免使用终结器,避免大对象,避免复制缓冲区

    避免使用终结器 如果没有必要,是不需要实现一个终结器(Finalizer).终结器的代码主要是让GC回收非托管资源用.它会在GC完成标记对象为可回收后,放入一个终结器队列里,在由另外一个线程执行队列里 ...

  6. [翻译] 编写高性能 .NET 代码--第二章 GC -- 将长生命周期对象和大对象池化

    将长生命周期对象和大对象池化 请记住最开始说的原则:对象要么立即回收要么一直存在.它们要么在0代被回收,要么在2代里一直存在.有些对象本质是静态的,生命周期从它们被创建开始,到程序停止才会结束.其它对 ...

  7. [翻译]编写高性能 .NET 代码 第一章:性能测试与工具 -- 平均值 vs 百分比

    <<返回目录 平均值 vs 百分比 在考虑要性能测试的目标值时,我们需要考虑用什么统计口径.大多数人都会首选平均值,但在大多数情况下,这个正确的,但你也应该适当的考虑百分数.但你有可用性的 ...

  8. [翻译]编写高性能 .NET 代码 第二章:垃圾回收 基本操作

    返回目录 基本操作 垃圾回收的算法细节还在不断完善中,性能还会有进一步的提升.下文介绍的内容在不同的.NET版本里会略有不同,但大方向是不会有变动的. 在.net进程里会管理2个类型的内存堆:托管和非 ...

  9. [翻译] 编写高性能 .NET 代码--第二章 GC -- 配置选项

    配置选项 在基于"less rope to hang yourself with"思想下,.NET 框架没有给开发提供很多太多的配置选项.但在大多数情况下,GC会跟你的硬件配置,及 ...

随机推荐

  1. 线段树(hdu 2795)

    Billboard Time Limit: 20000/8000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  2. Google Earth影像数据破解之旅

    "Zed, you are so excellent." 为什么要写这句英文?容我卖个关子稍后再解释. 相信大多数人都体验过Google Earth(简称GE),我对GE最初的印象 ...

  3. Android高效内存:让图片占用尽可能少的内存

    Android高效内存:让图片占用尽可能少的内存 一.让你的图片最小化 1.1 大图小图内存使用情况对比 大图:440 * 336    小图:220 * 168 小图的高宽都是大图的1/2--> ...

  4. Vue2.x中的父子组件相互通信

    业务场景:(这里指的是直接父子级关系的通信) 美女(子组件)将消息发送给大群(父组件) 大群(父组件)收到美女发送的消息后再回个信息给美女(子组件) 父组件 template <template ...

  5. Android ContentProvider详解

    1.概述 ContentProvider以Uri的形式对外提供数据,允许其他应用程序访问或者修改数据.也就是说你可以通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过 ...

  6. Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace 解决方法

    这个问题是在用到spring时,本地IDE里面跑的很正常,但是打包后在集群上运行时报错. 多方查找资料后确定了问题的根源,由于在依赖中调用了spring的许多包,会存在文件覆盖的情况. 具体是 这三个 ...

  7. 微信小程序之购物车功能

    前言 以往的购物车,基本都是通过大量的 DOM 操作来实现.微信小程序其实跟 vue.js 的用法非常像,接下来就看看小程序可以怎样实现购物车功能. 需求 先来弄清楚购物车的需求. 单选.全选和取消, ...

  8. 《快学Scala》——基础

    声明值和变量 用val定义的值实际上是常量.用var定义的值是变量.在scala中鼓励使用val.不需要给出值或变量的类型,这个信息可以通过初始化的表达式推断出来.(声明值和变量但不做初始化会报错) ...

  9. MYSQL CASE WHEN THEN END 用法

    SELECT qr_cash_record.*,CASE WHEN cashrecord_type = 3 THEN (SELECT product_title FROM qr_fundsupport ...

  10. 仿中关村win8频道(win8.zol.com.cn)下的tab效果

    最近觉得中关村win8频道下的那个Tab效果很好看. 一时兴起就自己做了一个.觉得还蛮不错的,特地来给大家分享一下.以下是相关的HTML页面写法: <div class="popula ...