How to improve the performance of .NET applications
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
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.
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.
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
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.
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.
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.
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