Home > Article > Backend Development > Practical tutorial on writing high-performance .NET
This almost needs no explanation. It reduces memory usage, which naturally reduces the pressure during GC recycling, and at the same time reduces memory fragmentation and CPU usage. You can use some methods to achieve this, but it may conflict with other designs.
You need to carefully examine each object as you design it and ask yourself:
Do I really need this object?
Is this field what I need?
Can I reduce the size of the array?
Can I reduce the size of primitives (replace Int64 with Int32, etc.)?
Are these objects only used in rare cases or only during initialization?
Is it possible to convert some classes into structures so that they can be allocated on the stack or become part of an object?
Am I allocating a large amount of memory but only using a tiny fraction of it?
Can I get relevant data from other places?
Little story: In a function that responds to requests on the server side, we found that some memory larger than the memory segment will be allocated in a request. This causes us to trigger a complete GC for every request. This is because the CLR requires all generation 0 objects to be in one memory segment. If the currently allocated memory segment is full, a new memory segment will be opened, and the original memory segment will be opened. The memory segment is recycled for 2 generations. This is not a good implementation since we have no other way than reducing memory allocation.
There is a basic rule for high-performance programming with garbage collection, which is actually a guiding rule for code design.
The objects to be collected are either in gen 0 or not at all.
Collect objects in gen 0 or not at all.
Different The thing is, you want an object to have an extremely short lifetime and never touch it during GC, or, if you can't do that, they should go to 2 generations as fast as possible and stay there forever. , will never be recycled. This means that you keep a reference to a long-lived object forever. Usually, it also means that objects can be reused, especially objects in large object heaps.
The collection of each higher generation of GC will be more time-consuming than the previous generation. If you want to keep many generation 0,1 and few generation 2 objects. Even if background GC is turned on to do 2 generations of recycling, it will consume a lot of CPU operations. You may prefer to use this part of the CPU to the application instead of GC.
Note You may have heard the saying that every 10 generation 0 collections will produce a 1 generation collection, and every 10 generation 1 collections will produce a 2 generation collection. This is actually incorrect, but you need to understand that you want to generate as many fast generation 0 collections as possible, and a small number of generation 2 collections.
You'd better avoid generation 1 recycling, mainly because objects that have been promoted from generation 0 to generation 1 will be transferred to generation 2 at this time. Generation 1 is a buffer for objects entering Generation 2.
Ideally, every object you allocate should end its life cycle before the next generation 0 collection. You can measure the time between GCs and compare it to the lifetime of objects in your application. Information on how to use tools to measure life cycles can be found at the end of this chapter.
You may not be used to thinking like this, but this rule cuts into every aspect of the application. You need to think about it often and make a fundamental change in your mentality, so that you can implement this most important rule.
The shorter the scope of an object, the smaller the chance it will be promoted to the next generation when the next GC occurs. In general, don't create objects until you need them.
At the same time, when the cost of object creation is so high, exceptions can be created earlier so that they will not interfere with other processing logic.
In addition, you must ensure that the object exits the scope as early as possible. For local variables, you can end their lifetime after the last use, or even before the method ends. You can use {} to enclose the code. This will not affect your execution, but the compiler will consider that the object in this scope has completed its life cycle and is no longer used. If you need to call an object's method, try to reduce the time interval between the first and last calls so that the GC can recycle the object as early as possible.
If the object is associated (referenced) with some objects that will be maintained for a long time, you need to release their reference relationships. You may have more null checks, which may make the code more complex. It can also create a tension between the object's available state (always having full state available) and efficiency, especially when debugging.
One solution is to convert the object to be cleared into another way, such as a log message, so that the relevant information can be queried during subsequent debugging.
Another method is to add configurable options to the code (without dissolving the relationship between objects): running the program (or running a specific part of the program, such as a specific request), there is no disassembly of the objects in this mode reference relationship, but to keep the object as convenient as possible for debugging.
As mentioned at the beginning of this chapter, the GC will traverse along the reference relationship of the object when recycling. In server GC mode, GC will run in a multi-threaded manner, but if a thread needs to process an object at a deep level, all threads that have processed it will need to wait for this thread to complete the processing before exiting. In future CLR versions, you don't need to pay too much attention to this issue. GC will use a better marking algorithm for load balancing during multi-thread execution. But if your object level is very deep, you still need to pay attention to this issue.
This is related to the depth of the previous section, but there are also some other factors.
If an object references many objects (array, List), it will spend a lot of time traversing the objects. It is the GC that causes a problem for a long time because it has a complex relationship graph.
Another problem is that if you cannot easily determine how many reference relationships an object has, then you cannot accurately predict the life cycle of the object. Reducing this complexity is quite necessary, not only to make the code more robust, but also to facilitate debugging and achieve better performance.
In addition, please note that references between objects of different generations will also cause inefficiency of GC, especially references from old objects to new objects. For example, if a 2nd generation object has a reference relationship in a 0th generation object, then every time a generation 0 GC occurs, some of the 2nd generation objects also need to be scanned to see if they still hold references to the 0th generation object. Although this is not a full GC, it is still unnecessary work and you should try to avoid this situation.
Pinning objects can ensure the safety of data transferred from managed code to local code. Common ones are arrays and strings. If your code doesn't need to interact with native code, don't worry about its performance overhead.
Pinning an object means that the object cannot be moved during garbage collection (compression phase). Although pinning objects does not cause much overhead, it will hinder GC recycling operations and increase the possibility of memory fragmentation. The GC will record the location of objects when recycling so that the space between them can be used when re-allocating. However, if there are many pinned objects, it will lead to an increase in memory fragmentation.
Nails can be explicit or implicit. What is shown is to set it with the GCHandleType.Pinned parameter when using GCHandle, or use the fixed keyword in unsafe mode. The difference between using the fixed keyword and GCHandle is whether the Dispose method will be called explicitly. Although using fixed is very convenient, it cannot be used in asynchronous situations. However, you can still create a handle object (GCHandle) and pass it back and process it during the callback.
Implicitly pinned objects are more common, but they are also more difficult to detect and remove. The most obvious example is passing objects to unmanaged code through platform calls (P/Invoke). This isn't just your code - some of the managed APIs you call frequently actually call native code and pin objects as well.
CLR will also pin some of its own data, but this usually doesn't require you to care.
Ideally, you should try not to pin objects as much as possible. If this is not possible, then follow the previous important rules and try to release these pinned objects as soon as possible. If the object is simply pinned and released, there is not much chance of affecting the collection operation. You also want to avoid pinning multiple objects at the same time. It is slightly better if pinned objects are swapped to generation 2 or allocated in LOH. According to this rule, you can allocate a large buffer on the large object heap and manage the buffer yourself according to actual needs. Or allocate buffers on pairs of small objects and then upgrade them to generation 2 before pinning them. This is better than pinning the object directly to generation 0.
The above is the detailed content of Practical tutorial on writing high-performance .NET. For more information, please follow other related articles on the PHP Chinese website!