Several design of good structures to improve .NET application performance

Original: Several ways to design a good structure to improve the performance of .NET applications

Written in front

A well-designed system, in addition to the excellent design at the architectural level, most of the rest lies in how to design well-designed code. NET provides many types, which are very flexible , Also very easy to use, such as List, Dictionary, HashSet, StringBuilder, string and so on. In most cases, everyone looks at the business needs to use it directly, and there seems to be no problem. From my actual experience, there are indeed very few problems. A friend asked me before, if I have encountered a memory leak, I said that the system I wrote did not, but a colleague wrote that I have encountered it several times.

In order to record the problems that have occurred, and to avoid similar problems in the future, I summarize this article and try to summarize several methods to effectively improve the performance of .NET from a statistical perspective.

This article is based on .NET Core 3.0 Preview4, and uses [Benchmark] for testing. If you don’t know Benchmark, I suggest you read this article after you finish.

collection-hidden initial capacity and automatic expansion

in .NET Here, the collection types List, Dictionary, and HashSet all have initial capacity. When the newly added data is larger than the initial capacity, it will be automatically expanded. Maybe you seldom pay attention to this hidden detail when using it (Not here yet. Consider the default initial capacity, load factor, expansion increment).

Automatic expansion gives users the perception of unlimited capacity. If it is not used very well, it may bring some new problems. Because whenever the newly added data of the collection is larger than the currently applied capacity, a larger memory capacity will be applied, which is generally twice the current capacity. This means that we may need additional memory overhead during the collection operation.

In this test, I used four scenarios, which may not be complete, but very illustrative. Each method is looped 1000 times, and the time complexity is all O(1000):

  • DynamicCapacity: do not set the default length
  • LargeFixedCapacity: the default length is 2000
  • FixedCapacity: the default length is 1000
  • FixedAndDynamicCapacity: The default length is 100

The following figure shows the test results of List, you can see that its comprehensive performance ranking is FixedCapacity>LargeFixedCapacity>DynamicCapacity>FixedAndDynamicCapacity

p>

list

The picture below is From the test results of Dictionary, it can be seen that its comprehensive performance ranking is FixedCapacity>LargeFixedCapacity>FixedAndDynamicCapacity>DynamicCapacity. In the Dictionary scene, the performance difference between the two methods of FixedAndDynamicCapacity and DynamicCapacity is not large, and it may be that the amount is not large enough

< p>dic

The following figure shows the test result of HashSet, you can see it The overall performance ranking is FixedCapacity>LargeFixedCapacity>FixedAndDynamicCapacity>DynamicCapacity. In the HashSet scene, the performance difference between the two methods of FixedAndDynamicCapacity and DynamicCapacity is still very large.

hashset

To sum up:

A proper initial value of capacity , Can effectively improve the efficiency of the collection operation. If it is not good to set an accurate data, you can apply for a slightly larger space than the actual one, but it will waste memory space and actually reduce the collection operation performance. You need to pay special attention when programming.

The following is the test source code of List, and the other two types of test codes are basically the same:

  1: public class ListTest
 2: {
 3:  private

span> int size = 1000;

 4: 
 5:  [Benchmark]
 6:  public< /span> void DynamicCapacity()
 7:  {
 8:  List<int> list = new List< int>();
 9:  for (int i = 0; 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 
 20:  {
 21:  list.Add(i);
 22:  }
  23:  }
 24: 
 25:  [Benchmark]
 26:  public < span class="kwrd">void FixedCapacity()
 27:  {
< span class="lnum"> 28:  List<int> list = new List<int>(size);
 29:  for< /span> (int i = 0; i 
 30:  {< /pre> 
 31:  list.Add(i);
 32: < /span> }
 33:  }
 34: 
 35:  [Benchmark]
 36:   public void FixedAndDynamicCapacity()
 37:  { 
 38:  List<int> list = new List<int>(100);
 39:  for (int i = 0; i 
 40:  {
 41:  list.Add(i);
 42:  }
 43:  }
 44: }

Structures and classes

Structures are value types, one of reference types and value types The difference is that reference types are allocated on the heap and garbage collected, while value types are allocated on the stack and released when the stack is expanded, or inline containing types and released when their containing types are released. Therefore, the allocation and release of value types are usually less expensive than the allocation and release of reference types.

Generally speaking, most types in the framework should be classes. However, in some cases, the characteristics of value types make it more suitable for using structures.

If the instance of the type is relatively small and usually has a short lifetime or is usually embedded in other objects, define the structure instead of the class.

This type has all the following characteristics and can define a structure:

  • It logically represents a single value, similar to the primitive type (int< /code>, double, etc.)

  • Its instance size is less than 16 bytes

  • < p>It is immutable

  • It will not be boxed frequently

In all other cases, it should be The type is defined as a class. Since the structure is copied when it is transferred, it may not be suitable for improving performance in some scenarios.

The above is taken from MSDN, you can click to view details

struct

It can be seen that the average allocation time of Struct is only one-sixth of Class.

The following is the test source code of this case:

 1: < span class="kwrd">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: < /span> for (int i = 0; i 
 24:  {
 25:  test[i].UserId = 1;
 26:  test[i].Age = 22;
 27: < /span> }
 28:  }
 29:  span>
 30:  [Benchmark]
 31:  public void TestByClass()
 32:  {
 33:  UserClassTest[] test = new UserClassTest[< span class="kwrd">this.size];
 34:  for ( int i = 0; i 
 35:  { 
 36:  test[i] = new UserClassTest
 37:  {
 38:  UserId = 1,
 39:  Age = 22
 40:  };
 41:  }
 42:  }
 43: }

StringBuilder and string

The string is immutable, and an object will be reassigned every time a value is assigned. When there are a large number of string operations, the use of string is very prone to memory overflow, such as exporting Excel operations, so StringBuilder is generally recommended for operations with a large number of strings. To improve system performance.

The following is the test result of one thousand executions. It can be seen that the memory allocation efficiency of StringBuilder objects is very high. Of course, this is in the case of a large number of string processing, and a small number of string operations can still be used string, its performance loss can be ignored

image

This is the case of five executions. It can be found that although the memory allocation time of string is still long, it is stable and has a low error rate.

image

The test code is as follows:

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

destructor

The destructor indicates that when the life cycle of a class has been called, it will automatically clean up the resources occupied by the object. The destructor method does not take any parameters, it actually guarantees that the garbage collection method Finalize() will be called in the program, and the object using the destructor will not be processed in G0, which means that the recycling of the object may be slower . Under normal circumstances, it is not recommended to use a destructor, and it is more recommended to use IDispose, and IDispose has just the versatility, can handle managed resources and unmanaged resources.

The following is the result of this test, you can see that the gap in the average memory allocation efficiency is still very large

des

The test code is as follows:

< span class="lnum"> 1: public class DestructionTest
 2: {
 3:  private int size = 5;
 4: 
 5:  [Benchmark]
 6:  public span> void NoDestruction()
 7:  {

< pre> 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: < /span> {

19: UserDestructionTest userTest = new UserDestructionTest();

 20:  }
 21:  }
 22: }
 23: 
 24: public class UserTest: IDisposable
  25: {
 26:  public < span class="kwrd">int UserId {get; set; }
 27: 

< pre> 28: public int Age {get; set; }< /pre>

 29: 
 30:  public void Dispose()
 31:  {
 32:  Console.WriteLine("11");

< pre class="alt"> 33: }

 34: }
 35: 
 36: public  class UserDestructionTest
 37: {

< pre> 38: ~UserDestructionTest()

 39:  {
 40: 
 41:  }

< pre> 42:

 43:   public int UserId {get; set; }
 44: 
 45:  public int Age {get; set; }
 46: }

 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 
  10:  {
 11:  list.Add(i); pre> 
 12:  }
 13:  }
 14: 
 15:  [Benchmark]
 16:  public void LargeFixedCapacity()
 17:  {
 18:  List<int> list = new List<int>(2000);
 19:  for (int i = 0; i 
 20:  {
 21:  list.Add(i);
< span class="lnum"> 22:  }
 23:  }
 24: 
 25:  [Benchmark]
 26:  public void FixedCapacity()
 27:  {
 28:  List<int > list = new List<int>(size);
 29:  for (int i = 0; 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  
 40:  {
 41:  list.Add(i );
 42:  }
 43:  }
 44: }

 1: public struct UserStructTest
 2: {
 3:  public int UserId {get;set ; }
 4: 
 5:  < span class="kwrd">public int Age {get; set; }
 6: < /span>}
 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 
 24:  {
 25:  test[i].UserId = 1;
 26:  test[i].Age = 22 ;
  27:          }
  28:      } 
  29:   
  30:      [Benchmark] 
  31:      public void TestByClass()
  32:      {
  33:  < /span>        Use rClassTest[] 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:  }

   1:  public class Stri ngBuilderTest
   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:  }

   1:  public class DestructionTest
   2:  {
   3:      private int size = 5;
   4:   
   5:      [Benchmark]
   6:      public void NoDestruction()
   7:      {
   8:          < span class="kwrd">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:  }

Leave a Comment

Your email address will not be published.