Written in front

Well-designed systems, in addition to the excellent design at the architectural level, the rest is mostly about how to design good code. .NET provides a lot of types, these types are very flexible and very easy to use, such as List, Dictionary, HashSet , StringBuilder, string, and more. In most cases, everyone is looking at the business and needs to go directly to it. It seems that there is no problem. From my practical experience, the situation is really rare. A friend asked me before, did I have encountered a memory leak, I said that the system I wrote did not, but I have encountered several times by colleagues.

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

This article is based on .NET Core 3.0 Preview4 and is tested with [Benchmark]. If you don’t understand Benchmark, it is recommended to read this article after you understand it.

Collection – hidden initial capacity and automatic expansion

In .NET, List, Dictionary, HashSet collection of these types have an initial capacity when new data is greater than the initial capacity, will be automatically extended, in time we may use a little attention to this hidden details ( here temporarily Consider the default initial capacity, load factor, expansion increment ).

The automatic expansion to the user’s perception is unlimited capacity, if not used very well, may bring some new problems. Because whenever the newly added data of the collection is larger than the current applied capacity, it will apply for a larger memory capacity, which is usually 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 very complete, but it is very illustrative. Each method is looped 1000 times, and the time complexity is 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


How_to_improve_the_performance_of_.NET_applications_0.png

 

The following picture shows the test results of Dictionary. It can be seen that the comprehensive performance ranking is FixedCapacity>LargeFixedCapacity>FixedAndDynamicCapacity>DynamicCapacity. In the Dictionary scene, the performance of the two methods of FixedAndDynamicCapacity and DynamicCapacity is not big, maybe the amount is not big enough.


How_to_improve_the_performance_of_.NET_applications_1.png

 

The following figure shows the test results of HashSet. It can be seen that the comprehensive performance ranking is FixedCapacity>LargeFixedCapacity>FixedAndDynamicCapacity>DynamicCapacity. In the HashSet scene, the performance of the two methods of FixedAndDynamicCapacity and DynamicCapacity is still very different.


How_to_improve_the_performance_of_.NET_applications_2.png

 

In summary:

An appropriate initial value of the capacity can effectively improve the efficiency of the collection operation. If you do not set an accurate data, you can apply a little larger space than the actual one, but it will waste memory space and actually reduce the performance of the collection operation. Special attention is required.

The following is the test source code for List. The other two types of test code are basically the same:

   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:       }
  twenty four:   
  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:   }

Structure and class

A structure is a value type. The difference between a reference type and a value type is that the reference type is allocated on the heap and garbage collected, while the value types are allocated on the stack and are released when the stack is expanded, or inline containing types and in them The containing type is released when it is released. Therefore, the allocation and release of value types is generally lower than the allocation and release overhead of reference types.

In general, most types in the framework should be classes. However, in some cases, the characteristics of the value type make it more suitable for use with the structure.

If the instance of the type is 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 of the following characteristics and can define a structure:

  • It logically represents a single value, similar to the primitive type ( int, double, etc.)
  • Its instance size is less than 16 bytes
  • It is immutable
  • It does not pack frequently

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

The above is taken from MSDN,
click to view details


How_to_improve_the_performance_of_.NET_applications_3.png

 

You can see that the average allocation time of Struct is only one-sixth of that of Class.

The following is the test source code for this case:

   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:   }

StringBuilder and string

Strings are immutable. Each time an assignment is reassigned an object. When there are a lot of string operations, using string is very prone to memory overflow, such as exporting Excel operations. Therefore, it is recommended to use StringBuilder for a large number of string operations. Improve system performance.

The following is the result of a thousand execution tests. You can see that the memory allocation efficiency of the StringBuilder object is very high. Of course, this is a case of a large number of string processing. A small number of string operations can still use string, and its performance loss can be ignored.


How_to_improve_the_performance_of_.NET_applications_4.png

 

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


How_to_improve_the_performance_of_.NET_applications_5.png

 

The test code is as follows:

   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:   }

Destructor

When the destructor identifies that a class’s lifecycle has been called, it automatically cleans up the resources occupied by the object. The destructor takes no arguments. It actually guarantees that the garbage collection method Finalize() will be called in the program. Objects using the destructor will not be processed in G0, which means that the object may be recycled slowly. . Normally, destructors are deprecated, and IDispose is preferred, and IDispose has just the versatility to handle both managed and unmanaged resources.

The results of this test are as follows. It can be seen that the gap in the average memory allocation efficiency is still very large.


How_to_improve_the_performance_of_.NET_applications_6.png

 

The test code is as follows:

   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:   }
  twenty three:   
  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:   }

Orignal link:https://www.cnblogs.com/edison0621/p/11069653.html