Given the massive amount of C# and the explosive growth of the data we're dealing with, some optimization work needs to be done at different times. Most of the big gains often come from actually rethinking problems and solving them from a whole new perspective.
Today I want to share some C# performance tips that have helped me with my latest work. Some of these features may seem rather trivial to you, so please don't charge here and use all of them. That's it, tip 1 is...
1. Every developer should use an analyzer
There are some great. NET parser. I personally used the Jet Brains[2] team's dotTrace analyzer. I know Jason on our team also gets a lot of value from the Red Gate analyzer[3]. Every developer should install and use the profiler.
I can't count the number of times I think the slowest part of the application is in one region, when in fact it's entirely elsewhere. The profiler helps with this. Also, sometimes, it can help me find mistakes – the slow part is slow only because it did something wrong.
This is the first step in all the optimization work you want to perform, and it is also an effective first step.
2. The higher the level of abstraction, the slower it is (usually)
It's just the smell I smell. The higher the level of abstraction you use, the slower it is usually. A common example I've found here is using LINQ in parts of code that are busy (perhaps millions of times in a loop). LINQ is great for expressing something quickly that can take a bunch of code, but you'll usually leave performance to the desktop.
Don't get me wrong – LINQ is great for allowing you to develop working applications. But in the performance-centric part of the codebase, you might pay too much. Especially since it's so easy to link so many operations together.
The specific example I use is where I use it. SelectMany(). Distinct(). Count()。 Given that this is known as tens of millions of times (a key hot spot discovered by my profiler), it's accumulating a lot of runtime. I took a different approach and reduced the execution time by several orders of magnitude.
3. Don't underestimate the release and debug versions
I've been working hard and am very happy with the performance I've gained. Then, I realized that I had done all my tests in Visual Studio (I often write performance tests as well that they can also be run as unit tests, so I can run the parts I care about more easily). We all know that the release version has optimization enabled.
So I made a release version called the Method of Application Testing from the Console.
I've made a big shift on that. My code has been wildly optimized so it's really time to get it right. The NET JIT compiler has made some micro-optimizations. After enabling optimization, my performance improved by about 30%! This reminds me of a story I read online not long ago.
This is an old game programming story from the 1990s, when memory limits were very tight. Later in the development cycle, the team will eventually run out of memory and start thinking about what must be removed or downgraded to fit the tiny memory space available. The veteran developer had expected this in his experience and allocated 1MB of memory and junk data at the beginning of the project. He then saved the day and removed the 1MB of memory he allocated immediately at the beginning of the project, thus solving the problem!
Knowing that the team always doesn't have enough space because there's memory available there, you can give the team what they need and ship it on time.
Why should I share this? Similar in terms of performance – get good enough running in debug mode, and you'll get some "free" performance in the release. Good times.
4. Look at the big picture
There are some great algorithms. Most of you don't need to use it every day or even month. However, it is worth knowing that they exist. I often do research and I find a better way to solve the problem. Developers who do research before coding are about the same as developers who do proper analysis before writing code. We love code and always want to go straight into the IDE.
In addition, often when looking at performance issues, we focus too much on a single line or method. This can be a mistake – looking at the big picture can help you significantly improve performance by reducing the amount of work that needs to be done.
5. Memory location is important
Suppose we have an array array. It's actually a table with dimensions of 3000×3000. We want to calculate how many slots have values greater than zero.
Question – Which of the two is faster?
Reply? The first one. In my tests, this loop improved performance by a factor of 8!
Notice the difference? This is the order in which we iterate over this array array ([i][n] and [n][i]). Even if we abstract from our own management memory, memory locality is in . IT's really important in NET.
In my case, this approach is called millions (hundreds of millions of times to be precise), so any performance I can get out of it is a considerable win. Thanks again for the analyzer I use a lot to make sure I'm focused on the right place!
6. Reduce the pressure on the garbage collector
C#/. NET has a garbage collection feature. Garbage collection is the process of determining which objects are currently obsolete and deleting them to free up space in memory. This means that in C#, unlike languages like C++, you don't have to manually maintain the deletion of objects that are no longer useful to declare their space in memory. Instead, the garbage collector (GC) handles all of this, so you don't have to.
The problem is that there is no free lunch. The collection process itself causes performance degradation, so you don't actually want the GC to collect all the time. So how do you avoid this?
There are many useful techniques to avoid putting too much pressure on the GC [4]. Here, I'll focus on just one trick: avoid unnecessary allocations. This means avoiding things like this:
The first row creates a completely useless list instance because the next row returns another instance and assigns a reference to the variable. Now imagine if the two lines above are in a loop that executes thousands of times?
The code above may seem like a silly example, but I've seen it in production, not just once. Focus not only on the examples themselves, but on general recommendations. Do not create an object unless it is really needed.
Because of the way GC works in .NET (which is a generational GC process), older objects are more likely to collect newer objects. This means that creating many new, ephemeral objects may trigger a GC run.
7. Do not use an empty destructor
The title says it all - don't add an empty destructor to your class. Finalize The entry for each class with a destructor is added to the queue. Then call our old friend GC to handle the queue when calling the destructor. An empty destructor means that it's all in vain.
Keep in mind that GC execution is not cheap in terms of performance, as we have already mentioned. Do not unnecessarily lead to GC work.
8. Avoid unnecessary packing and unboxing
Binning and unpacking is like garbage collection, which is expensive in terms of performance. Therefore, we want to avoid unnecessary operations. But what will they do in practice?
Binning is like creating a reference type box and putting the value of the value type into it. In other words, it includes converting a value type to an "object" or an interface type that that the value type implements. Instead, it opens the box and extracts the value type from it. Why is there a problem?
Well, as we have already mentioned, boxing and unboxing is an expensive process in itself. In addition to that, when you box one value, you create another object on the heap, which puts extra pressure on the GC (you've already guessed it!). )。
So, how do you avoid boxing and unboxing?
Usually you can avoid it by . APIs in NET (version 1.0) that predate generics do this, so they must rely on using object types. For example, generic collections, such as System.Collections.Generic.List, are preferred over System.Collections.ArrayList.
9. Beware of string concatenation
In C#/. In NET, strings are immutable. Therefore, every time you perform some operation that looks as if you are changing a string, they create a new string. These operations include similar methods of Place and Substring, which are also concatenated.
Beware of concatenating large numbers of strings, especially inside loops
Therefore, the trick here is simple - be careful not to concatenate a large number of strings, especially inside the loop. In this case, use the System.Text.StringBuilder class instead of the "+" operator. This ensures that no new instances are created for each part of the connection.
10. Stay tuned for C# developments
Finally, we end with very general advice – keep an eye on how the C# language changes and evolves. The C# team is constantly delivering new features that can have a positive impact on performance.
One of the latest examples we can mention is the introduction of ref[5] return and ref locals[6] introduced in C#7. These new features allow developers to return by reference and store references in local variables. C# 7.2 introduced the Span[7] type, which allows type-safe access to contiguous regions of memory.
New features and types such as these are unlikely to be used by most C# developers, but they will undoubtedly have an impact on performance-critical applications and are worth learning more about.
C# performance matters!
This is just what I found on the improvement. NET code performance is a collection of several things that are useful - but it's worth taking the time to check your code to ensure its performance. Your team and customers will thank you!
References
[1] Raygun: https://raygun.com/[2] Jet Brains: https://www.jetbrains.com/[3] In Red Gate Analyzer: http://www.red-gate.com/products/dotnet-development/ants-performance-profiler/[4] Many useful techniques can avoid putting too much pressure on the GC: https://michaelscodingspot.com/avoid-gc-pressure/[5] ref: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/ ref-returns[6] and ref locals: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/ref-returns[7] Span: https://docs.microsoft.com/en-us/dotnet/api/system.span-1?view=netcore-3.0
The article comes from dotnet technology circles and is written by Raygun