前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >几种设计良好结构以提高.NET性能的方法

几种设计良好结构以提高.NET性能的方法

作者头像
Edison.Ma
发布2019-07-19 17:58:02
5320
发布2019-07-19 17:58:02
举报
文章被收录于专栏:DotNet Core圈圈DotNet Core圈圈

写在前面

设计良好的系统,除了架构层面的优良设计外,剩下的大部分就在于如何设计良好的代码,.NET提供了很多的类型,这些类型非常灵活,也非常好用,比如List,Dictionary、HashSet、StringBuilder、string等等。在大多数情况下,大家都是看着业务需要直接去用,似乎并没有什么问题。从我的实际经验来看,出现问题的情况确实是少之又少。之前有朋友问我,我有没有遇到过内存泄漏的情况,我说我写的系统没有,但是同事写的我遇到过几次。

为了记录曾经发生的问题,也为了以后可以避免类似的问题,总结这篇文章,力图从数据统计角度总结几个有效提升.NET性能的方法。

本文基于.NET Core 3.0 Preview4,采用[Benchmark]进行测试,如果不了解Benchmark,建议了解完之后再看本文。

集合-隐藏的初始容量及自动扩容

在.NET里,List、Dictionary、HashSet这些集合类型都具有初始容量,当新增的数据大于初始容量时,会自动扩展,可能大家在使用的时候很少注意这个隐藏的细节(此处暂不考虑默认初始容量、加载因子、扩容增量)。

自动扩容给使用者的感知是无限容量,如果用的不是很好,可能会带来一些新的问题。因为每当集合新增的数据大于当前已经申请的容量的时候,会再申请更大的内存容量,一般是当前容量的两倍。这就意味着我们在集合操作过程中可能需要额外的内存开销。

在本次测试中,我用到了四种场景,可能并不是很完全,但是很有说明性,每个方法都是循环了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的两个方法性能相差还是很大的

综上所述: 一个恰当的容量初始值,可以有效提升集合操作的效率,如果不太好设置一个准确的数据,可以申请比实际稍大的空间,但是会浪费内存空间,并在实际上降低集合操作性能,编程的时候需要特别注意。

以下是List的测试源码,另两种类型的测试代码与之基本一致:

代码语言:javascript
复制
   1:  public class ListTest
代码语言:javascript
复制
   2:  {
代码语言:javascript
复制
   3:      private int size = 1000;
代码语言:javascript
复制
   4:   
代码语言:javascript
复制
   5:      [Benchmark]
代码语言:javascript
复制
   6:      public void DynamicCapacity()
代码语言:javascript
复制
   7:      {
代码语言:javascript
复制
   8:          List<int> list = new List<int>();
代码语言:javascript
复制
   9:          for (int i = 0; i < size; i++)
代码语言:javascript
复制
  10:          {
代码语言:javascript
复制
  11:              list.Add(i);
代码语言:javascript
复制
  12:          }
代码语言:javascript
复制
  13:      }
代码语言:javascript
复制
  14:   
代码语言:javascript
复制
  15:      [Benchmark]
代码语言:javascript
复制
  16:      public void LargeFixedCapacity()
代码语言:javascript
复制
  17:      {
代码语言:javascript
复制
  18:          List<int> list = new List<int>(2000);
代码语言:javascript
复制
  19:          for (int i = 0; i < size; i++)
代码语言:javascript
复制
  20:          {
代码语言:javascript
复制
  21:              list.Add(i);
代码语言:javascript
复制
  22:          }
代码语言:javascript
复制
  23:      }
代码语言:javascript
复制
  24:   
代码语言:javascript
复制
  25:      [Benchmark]
代码语言:javascript
复制
  26:      public void FixedCapacity()
代码语言:javascript
复制
  27:      {
代码语言:javascript
复制
  28:          List<int> list = new List<int>(size);
代码语言:javascript
复制
  29:          for (int i = 0; i < size; i++)
代码语言:javascript
复制
  30:          {
代码语言:javascript
复制
  31:              list.Add(i);
代码语言:javascript
复制
  32:          }
代码语言:javascript
复制
  33:      }
代码语言:javascript
复制
  34:   
代码语言:javascript
复制
  35:      [Benchmark]
代码语言:javascript
复制
  36:      public void FixedAndDynamicCapacity()
代码语言:javascript
复制
  37:      {
代码语言:javascript
复制
  38:          List<int> list = new List<int>(100);
代码语言:javascript
复制
  39:          for (int i = 0; i < size; i++)
代码语言:javascript
复制
  40:          {
代码语言:javascript
复制
  41:              list.Add(i);
代码语言:javascript
复制
  42:          }
代码语言:javascript
复制
  43:      }
代码语言:javascript
复制
  44:  }

结构体与类

结构体是值类型,引用类型和值类型之间的区别是引用类型在堆上分配并进行垃圾回收,而值类型在堆栈中分配并在堆栈展开时被释放,或内联包含类型并在它们的包含类型被释放时被释放。因此,值类型的分配和释放通常比引用类型的分配和释放开销更低。

一般来说,框架中的大多数类型应该是类。但是,在某些情况下,值类型的特征使得其更适合使用结构。

如果类型的实例比较小并且通常生存期较短或者通常嵌入在其他对象中,则定义结构而不是类。

该类型具有所有以下特征,可以定义一个结构:

  • 它逻辑上表示单个值,类似于基元类型(intdouble,等等)
  • 它的实例大小小于 16 字节
  • 它是不可变的
  • 它不会频繁装箱

在所有其他情况下,应将类型定义为类。由于结构体在传递的时候,会被复制,因此在某些场景下可能并不适合提升性能。

以上摘自MSDN,可点击查看详情

可以看到Struct的平均分配时间只有Class的六分之一。

以下为该案例的测试源码:

代码语言:javascript
复制
   1:  public struct UserStructTest
代码语言:javascript
复制
   2:  {
代码语言:javascript
复制
   3:      public int UserId { get;set; }
代码语言:javascript
复制
   4:   
代码语言:javascript
复制
   5:      public int Age { get; set; }
代码语言:javascript
复制
   6:  }
代码语言:javascript
复制
   7:   
代码语言:javascript
复制
   8:  public class UserClassTest
代码语言:javascript
复制
   9:  {
代码语言:javascript
复制
  10:      public int UserId { get; set; }
代码语言:javascript
复制
  11:   
代码语言:javascript
复制
  12:      public int Age { get; set; }
代码语言:javascript
复制
  13:  }
代码语言:javascript
复制
  14:   
代码语言:javascript
复制
  15:  public class StructTest
代码语言:javascript
复制
  16:  {
代码语言:javascript
复制
  17:      private int size = 1000;
代码语言:javascript
复制
  18:   
代码语言:javascript
复制
  19:      [Benchmark]
代码语言:javascript
复制
  20:      public void TestByStruct()
代码语言:javascript
复制
  21:      {
代码语言:javascript
复制
  22:          UserStructTest[] test = new UserStructTest[this.size];
代码语言:javascript
复制
  23:          for (int i = 0; i < size; i++)
代码语言:javascript
复制
  24:          {
代码语言:javascript
复制
  25:              test[i].UserId = 1;
代码语言:javascript
复制
  26:              test[i].Age = 22;
代码语言:javascript
复制
  27:          }
代码语言:javascript
复制
  28:      }
代码语言:javascript
复制
  29:   
代码语言:javascript
复制
  30:      [Benchmark]
代码语言:javascript
复制
  31:      public void TestByClass()
代码语言:javascript
复制
  32:      {
代码语言:javascript
复制
  33:          UserClassTest[] test = new UserClassTest[this.size];
代码语言:javascript
复制
  34:          for (int i = 0; i < size; i++)
代码语言:javascript
复制
  35:          {
代码语言:javascript
复制
  36:              test[i] = new UserClassTest
代码语言:javascript
复制
  37:              {
代码语言:javascript
复制
  38:                  UserId = 1,
代码语言:javascript
复制
  39:                  Age = 22
代码语言:javascript
复制
  40:              };
代码语言:javascript
复制
  41:          }
代码语言:javascript
复制
  42:      }
代码语言:javascript
复制
  43:  }

StringBuilder与string

字符串是不可变的,每次的赋值都会重新分配一个对象,当有大量字符串操作时,使用string非常容易出现内存溢出,比如导出Excel操作,所以大量字符串的操作一般推荐使用StringBuilder,以提高系统性能。

以下为一千次执行的测试结果,可以看到StringBuilder对象的内存分配效率十分的高,当然这是在大量字符串处理的情况,少部分的字符串操作依然可以使用string,其性能损耗可以忽略

这是执行五次的情况,可以发现虽然string的内存分配时间依然较长,但是稳定且错误率低

测试代码如下:

代码语言:javascript
复制
   1:  public class StringBuilderTest
代码语言:javascript
复制
   2:  {
代码语言:javascript
复制
   3:      private int size = 5;
代码语言:javascript
复制
   4:   
代码语言:javascript
复制
   5:      [Benchmark]
代码语言:javascript
复制
   6:      public void TestByString()
代码语言:javascript
复制
   7:      {
代码语言:javascript
复制
   8:          string s = string.Empty;
代码语言:javascript
复制
   9:          for (int i = 0; i < size; i++)
代码语言:javascript
复制
  10:          {
代码语言:javascript
复制
  11:              s += "a";
代码语言:javascript
复制
  12:              s += "b";
代码语言:javascript
复制
  13:          }
代码语言:javascript
复制
  14:      }
代码语言:javascript
复制
  15:   
代码语言:javascript
复制
  16:      [Benchmark]
代码语言:javascript
复制
  17:      public void TestByStringBuilder()
代码语言:javascript
复制
  18:      {
代码语言:javascript
复制
  19:          StringBuilder sb = new StringBuilder();
代码语言:javascript
复制
  20:          for (int i = 0; i < size; i++)
代码语言:javascript
复制
  21:          {
代码语言:javascript
复制
  22:              sb.Append("a");
代码语言:javascript
复制
  23:              sb.Append("b");
代码语言:javascript
复制
  24:          }
代码语言:javascript
复制
  25:   
代码语言:javascript
复制
  26:          string s = sb.ToString();
代码语言:javascript
复制
  27:      }
代码语言:javascript
复制
  28:  }

析构函数

析构函数标识了一个类的生命周期已调用完毕时,会自动清理对象所占用的资源。析构方法不带任何参数,它实际上是保证在程序中会调用垃圾回收方法 Finalize(),使用析构函数的对象不会在G0中处理,这就意味着该对象的回收可能会比较慢。通常情况下,不建议使用析构函数,更推荐使用IDispose,而且IDispose具有刚好的通用性,可以处理托管资源和非托管资源。

以下为本次测试的结果,其内存分配的差距非常大

测试代码如下:

代码语言:javascript
复制
   1:  public class DestructionTest
代码语言:javascript
复制
   2:  {
代码语言:javascript
复制
   3:      private int size = 5;
代码语言:javascript
复制
   4:   
代码语言:javascript
复制
   5:      [Benchmark]
代码语言:javascript
复制
   6:      public void NoDestruction()
代码语言:javascript
复制
   7:      {
代码语言:javascript
复制
   8:          for (int i = 0; i < this.size; i++)
代码语言:javascript
复制
   9:          {
代码语言:javascript
复制
  10:              UserTest userTest = new UserTest();
代码语言:javascript
复制
  11:          }
代码语言:javascript
复制
  12:      }
代码语言:javascript
复制
  13:   
代码语言:javascript
复制
  14:      [Benchmark]
代码语言:javascript
复制
  15:      public void Destruction()
代码语言:javascript
复制
  16:      {
代码语言:javascript
复制
  17:          for (int i = 0; i < this.size; i++)
代码语言:javascript
复制
  18:          {
代码语言:javascript
复制
  19:              UserDestructionTest userTest = new UserDestructionTest();
代码语言:javascript
复制
  20:          }
代码语言:javascript
复制
  21:      }
代码语言:javascript
复制
  22:  }
代码语言:javascript
复制
  23:   
代码语言:javascript
复制
  24:  public class UserTest: IDisposable
代码语言:javascript
复制
  25:  {
代码语言:javascript
复制
  26:      public int UserId { get; set; }
代码语言:javascript
复制
  27:   
代码语言:javascript
复制
  28:      public int Age { get; set; }
代码语言:javascript
复制
  29:   
代码语言:javascript
复制
  30:      public void Dispose()
代码语言:javascript
复制
  31:      {
代码语言:javascript
复制
  32:          Console.WriteLine("11");
代码语言:javascript
复制
  33:      }
代码语言:javascript
复制
  34:  }
代码语言:javascript
复制
  35:   
代码语言:javascript
复制
  36:  public class UserDestructionTest
代码语言:javascript
复制
  37:  {
代码语言:javascript
复制
  38:      ~UserDestructionTest()
代码语言:javascript
复制
  39:      {
代码语言:javascript
复制
  40:   
代码语言:javascript
复制
  41:      }
代码语言:javascript
复制
  42:   
代码语言:javascript
复制
  43:      public int UserId { get; set; }
代码语言:javascript
复制
  44:   
代码语言:javascript
复制
  45:      public int Age { get; set; }
代码语言:javascript
复制
  46:  }
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet技术平台 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 集合-隐藏的初始容量及自动扩容
  • 结构体与类
  • StringBuilder与string
  • 析构函数
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档