设计良好的系统,除了架构层面的优良设计外,剩下的大部分就在于如何设计良好的代码,.NET提供了不少的类型,这些类型很是灵活,也很是好用,好比List,Dictionary、HashSet、StringBuilder、string等等。在大多数状况下,你们都是看着业务须要直接去用,彷佛并无什么问题。从个人实际经验来看,出现问题的状况确实是少之又少。以前有朋友问我,我有没有遇到过内存泄漏的状况,我说我写的系统没有,可是同事写的我遇到过几回。html
为了记录曾经发生的问题,也为了之后能够避免相似的问题,总结这篇文章,力图从数据统计角度总结几个有效提高.NET性能的方法。编程
本文基于.NET Core 3.0 Preview4,采用[Benchmark]进行测试,若是不了解Benchmark,建议了解完以后再看本文。架构
在.NET里,List、Dictionary、HashSet这些集合类型都具备初始容量,当新增的数据大于初始容量时,会自动扩展,可能你们在使用的时候不多注意这个隐藏的细节(此处暂不考虑默认初始容量、加载因子、扩容增量)。框架
自动扩容给使用者的感知是无限容量,若是用的不是很好,可能会带来一些新的问题。由于每当集合新增的数据大于当前已经申请的容量的时候,会再申请更大的内存容量,通常是当前容量的两倍。这就意味着咱们在集合操做过程当中可能须要额外的内存开销。ide
在本次测试中,我用到了四种场景,可能并非很彻底,可是颇有说明性,每一个方法都是循环了1000次,时间复杂度均为O(1000):函数
- DynamicCapacity:不设置默认长度
- LargeFixedCapacity:默认长度为2000
- FixedCapacity:默认长度为1000
- FixedAndDynamicCapacity:默认长度为100
下图为List的测试结果,能够看到其综合性能排名是FixedCapacity>LargeFixedCapacity>DynamicCapacity>FixedAndDynamicCapacity性能
下图为Dictionary的测试结果,能够看到其综合性能排名是FixedCapacity>LargeFixedCapacity>FixedAndDynamicCapacity>DynamicCapacity,在Dictionary场景中,FixedAndDynamicCapacity和DynamicCapacity的两个方法性能相差并不大,多是量还不够大测试
下图为HashSet的测试结果,能够看到其综合性能排名是FixedCapacity>LargeFixedCapacity>FixedAndDynamicCapacity>DynamicCapacity,在HashSet场景中,FixedAndDynamicCapacity和DynamicCapacity的两个方法性能相差仍是很大的ui
综上所述:this
一个恰当的容量初始值,能够有效提高集合操做的效率,若是不太好设置一个准确的数据,能够申请比实际稍大的空间,可是会浪费内存空间,并在实际上下降集合操做性能,编程的时候须要特别注意。
如下是List的测试源码,另两种类型的测试代码与之基本一致:
1: public class ListTest
2: {
3: private int size = 1000;
4:
5: [Benchmark]
6: public void DynamicCapacity()
7: {
8: List<int> list = new List<int>();
9: for (int i = 0; i < size; i++)
10: {
11: list.Add(i);
12: }
13: }
14:
15: [Benchmark]
16: public void LargeFixedCapacity()
17: {
18: List<int> list = new List<int>(2000);
19: for (int i = 0; i < size; i++)
20: {
21: list.Add(i);
22: }
23: }
24:
25: [Benchmark]
26: public void FixedCapacity()
27: {
28: List<int> list = new List<int>(size);
29: for (int i = 0; i < size; i++)
30: {
31: list.Add(i);
32: }
33: }
34:
35: [Benchmark]
36: public void FixedAndDynamicCapacity()
37: {
38: List<int> list = new List<int>(100);
39: for (int i = 0; i < size; i++)
40: {
41: list.Add(i);
42: }
43: }
44: }
结构体是值类型,引用类型和值类型之间的区别是引用类型在堆上分配并进行垃圾回收,而值类型在堆栈中分配并在堆栈展开时被释放,或内联包含类型并在它们的包含类型被释放时被释放。 所以,值类型的分配和释放一般比引用类型的分配和释放开销更低。
通常来讲,框架中的大多数类型应该是类。 可是,在某些状况下,值类型的特征使得其更适合使用结构。
若是类型的实例比较小而且一般生存期较短或者一般嵌入在其余对象中,则定义结构而不是类。
该类型具备全部如下特征,能够定义一个结构:
它逻辑上表示单个值,相似于基元类型(int
, double
,等等)
它的实例大小小于 16 字节
它是不可变的
它不会频繁装箱
在全部其余状况下,应将类型定义为类。因为结构体在传递的时候,会被复制,所以在某些场景下可能并不适合提高性能。
以上摘自MSDN,可点击查看详情
能够看到Struct的平均分配时间只有Class的六分之一。
如下为该案例的测试源码:
1: public struct UserStructTest
2: {
3: public int UserId { get;set; }
4:
5: public int Age { get; set; }
6: }
7:
8: public class UserClassTest
9: {
10: public int UserId { get; set; }
11:
12: public int Age { get; set; }
13: }
14:
15: public class StructTest
16: {
17: private int size = 1000;
18:
19: [Benchmark]
20: public void TestByStruct()
21: {
22: UserStructTest[] test = new UserStructTest[this.size];
23: for (int i = 0; i < size; i++)
24: {
25: test[i].UserId = 1;
26: test[i].Age = 22;
27: }
28: }
29:
30: [Benchmark]
31: public void TestByClass()
32: {
33: UserClassTest[] test = new UserClassTest[this.size];
34: for (int i = 0; i < size; i++)
35: {
36: test[i] = new UserClassTest
37: {
38: UserId = 1,
39: Age = 22
40: };
41: }
42: }
43: }
字符串是不可变的,每次的赋值都会从新分配一个对象,当有大量字符串操做时,使用string很是容易出现内存溢出,好比导出Excel操做,因此大量字符串的操做通常推荐使用StringBuilder,以提升系统性能。
如下为一千次执行的测试结果,能够看到StringBuilder对象的内存分配效率十分的高,固然这是在大量字符串处理的状况,少部分的字符串操做依然可使用string,其性能损耗能够忽略
这是执行五次的状况,能够发现虽然string的内存分配时间依然较长,可是稳定且错误率低
测试代码以下:
1: public class StringBuilderTest
2: {
3: private int size = 5;
4:
5: [Benchmark]
6: public void TestByString()
7: {
8: string s = string.Empty;
9: for (int i = 0; i < size; i++)
10: {
11: s += "a";
12: s += "b";
13: }
14: }
15:
16: [Benchmark]
17: public void TestByStringBuilder()
18: {
19: StringBuilder sb = new StringBuilder();
20: for (int i = 0; i < size; i++)
21: {
22: sb.Append("a");
23: sb.Append("b");
24: }
25:
26: string s = sb.ToString();
27: }
28: }
析构函数标识了一个类的生命周期已调用完毕时,会自动清理对象所占用的资源。析构方法不带任何参数,它其实是保证在程序中会调用垃圾回收方法 Finalize(),使用析构函数的对象不会在G0中处理,这就意味着该对象的回收可能会比较慢。一般状况下,不建议使用析构函数,更推荐使用IDispose,并且IDispose具备恰好的通用性,能够处理托管资源和非托管资源。
如下为本次测试的结果,能够看到内存平均分配效率的差距仍是很大的
测试代码以下:
1: public class DestructionTest
2: {
3: private int size = 5;
4:
5: [Benchmark]
6: public void NoDestruction()
7: {
8: for (int i = 0; i < this.size; i++)
9: {
10: UserTest userTest = new UserTest();
11: }
12: }
13:
14: [Benchmark]
15: public void Destruction()
16: {
17: for (int i = 0; i < this.size; i++)
18: {
19: UserDestructionTest userTest = new UserDestructionTest();
20: }
21: }
22: }
23:
24: public class UserTest: IDisposable
25: {
26: public int UserId { get; set; }
27:
28: public int Age { get; set; }
29:
30: public void Dispose()
31: {
32: Console.WriteLine("11");
33: }
34: }
35:
36: public class UserDestructionTest
37: {
38: ~UserDestructionTest()
39: {
40:
41: }
42:
43: public int UserId { get; set; }
44:
45: public int Age { get; set; }
46: }