




















































There are many factors that negatively impact the performance of a .NET Core application. Sometimes these are minor things that were not considered earlier at the time of writing the code, and are not addressed by the accepted best practices. As a result, to solve these problems, programmers often resort to ad hoc solutions. However, when bad practices are combined together, they produce performance issues. It is always better to know the best practices that help developers write cleaner code and make the application performant.
In this article, we will learn the following topics:
This tutorial is an extract from the book, C# 7 and .NET Core 2.0 High Performance, authored by Ovais Mehboob Ahmed Khan.
The boxing and unboxing methods are not always good to use and they negatively impact the performance of mission-critical applications. Boxing is a method of converting a value type to an object type, and is done implicitly, whereas unboxing is a method of converting an object type back to a value type and requires explicit casting.
Let's go through an example where we have two methods executing a loop of 10 million records, and in each iteration, they are incrementing the counter by 1. The AvoidBoxingUnboxing method is using a primitive integer to initialize and increment it on each iteration, whereas the BoxingUnboxing method is boxing by assigning the numeric value to the object type first and then unboxing it on each iteration to convert it back to the integer type, as shown in the following code:
private static void AvoidBoxingUnboxing() { Stopwatch watch = new Stopwatch(); watch.Start(); //Boxing int counter = 0; for (int i = 0; i < 1000000; i++) { //Unboxing counter = i + 1; } watch.Stop(); Console.WriteLine($"Time taken {watch.ElapsedMilliseconds}"); } private static void BoxingUnboxing() { Stopwatch watch = new Stopwatch(); watch.Start(); //Boxing object counter = 0; for (int i = 0; i < 1000000; i++) { //Unboxing counter = (int)i + 1; } watch.Stop(); Console.WriteLine($"Time taken {watch.ElapsedMilliseconds}"); }
When we run both methods, we will clearly see the differences in performance. The BoxingUnboxing is executed seven times slower than the AvoidBoxingUnboxing method, as shown in the following screenshot:
For mission-critical applications, it's always better to avoid boxing and unboxing. However, in .NET Core, we have many other types that internally use objects and perform boxing and unboxing. Most of the types under System.Collections and System.Collections.Specialized use objects and object arrays for internal storage, and when we store primitive types in these collections, they perform boxing and convert each primitive value to an object type, adding extra overhead and negatively impacting the performance of the application. Other types of System.Data, namely DateSet, DataTable, and DataRow, also use object arrays under the hood.
Types under the System.Collections.Generic namespace or typed arrays are the best approaches to use when performance is the primary concern. For example, HashSet<T>, LinkedList<T>, and List<T> are all types of generic collections.
For example, here is a program that stores the integer value in ArrayList:
private static void AddValuesInArrayList() { Stopwatch watch = new Stopwatch(); watch.Start(); ArrayList arr = new ArrayList(); for (int i = 0; i < 1000000; i++) { arr.Add(i); } watch.Stop(); Console.WriteLine($"Total time taken is {watch.ElapsedMilliseconds}"); }
Let's write another program that uses a generic list of the integer type:
private static void AddValuesInGenericList() { Stopwatch watch = new Stopwatch(); watch.Start(); List<int> lst = new List<int>(); for (int i = 0; i < 1000000; i++) { lst.Add(i); } watch.Stop(); Console.WriteLine($"Total time taken is {watch.ElapsedMilliseconds}"); }
When running both programs, the differences are pretty noticeable. The code with the generic list List<int> is over 10 times faster than the code with ArrayList. The result is as follows:
In .NET, strings are immutable objects. Two strings refer to the same memory on the heap until the string value is changed. If any of the string is changed, a new string is created on the heap and is allocated a new memory space. Immutable objects are generally thread safe and eliminate the race conditions between multiple threads. Any change in the string value creates and allocates a new object in memory and avoids producing conflicting scenarios with multiple threads.
For example, let's initialize the string and assign the Hello World value to the a string variable:
String a = "Hello World";
Now, let's assign the a string variable to another variable, b:
String b = a;
Both a and b point to the same value on the heap, as shown in the following diagram:
Now, suppose we change the value of b to Hope this helps:
b= "Hope this helps";
This will create another object on the heap, where a points to the same and b refers to the new memory space that contains the new text:
With each change in the string, the object allocates a new memory space. In some cases, it may be an overkill scenario, where the frequency of string modification is higher and each modification is allocated a separate memory space, creates work for the garbage collector in collecting the unused objects and freeing up space. In such a scenario, it is highly recommended that you use the StringBuilder class.
Improper handling of exceptions also decreases the performance of an application. The following list contains some of the best practices in dealing with exceptions in .NET Core:
static string GetCountryDetails(Dictionary<string, string> countryDictionary, string key) { try { return countryDictionary[key]; } catch (KeyNotFoundException ex) { KeyNotFoundException argEx = new KeyNotFoundException(" Error occured while executing GetCountryDetails method. Cause: Key not found", ex); throw argEx; } }
For and foreach are two of the alternative ways of iterating over a list of items. Each of them operates in a different way. The for loop actually loads all the items of the list in memory first and then uses an indexer to iterate over each element, whereas foreach uses an enumerator and iterates until it reaches the end of the list.
The following table shows the types of collections that are good to use for for and foreach:
Type | For/Foreach |
Typed array | Good for both |
Array list | Better with for |
Generic collections | Better with for |
Delegates are a type in .NET which hold the reference to the method. The type is equivalent to the function pointer in C or C++. When defining a delegate, we can specify both the parameters that the method can take and its return type. This way, the reference methods will have the same signature.
Here is a simple delegate that takes a string and returns an integer:
delegate int Log(string n);
Now, suppose we have a LogToConsole method that has the same signature as the one shown in the following code. This method takes the string and writes it to the console window:
static int LogToConsole(string a) { Console.WriteLine(a); return 1; }
We can initialize and use this delegate like this:
Log logDelegate = LogToConsole; logDelegate ("This is a simple delegate call");
Suppose we have another method called LogToDatabase that writes the information in the database:
static int LogToDatabase(string a) { Console.WriteLine(a); //Log to database return 1; }
Here is the initialization of the new logDelegate instance that references the LogToDatabase method:
Log logDelegateDatabase = LogToDatabase; logDelegateDatabase ("This is a simple delegate call");
The preceding delegate is the representation of unicast delegates, as each instance refers to a single method. On the other hand, we can also create multicast delegates by assigning LogToDatabase to the same LogDelegate instance, as follows:
Log logDelegate = LogToConsole; logDelegate += LogToDatabase; logDelegate("This is a simple delegate call");
The preceding code seems pretty straightforward and optimized, but under the hood, it has a huge performance overhead. In .NET, delegates are implemented by a MutlicastDelegate class that is optimized to run unicast delegates. It stores the reference of the method to the target property and calls the method directly. For multicast delegates, it uses the invocation list, which is a generic list, and holds the references to each method that is added. With multicast delegates, each target property holds the reference to the generic list that contains the method and executes in sequence. However, this adds an overhead for multicast delegates and takes more time to execute.
If you liked this article and would like to learn more such techniques, grab this book, C# 7 and .NET Core 2.0 High Performance, authored by Ovais Mehboob Ahmed Khan.
Behavior Scripting in C# and Javascript for game developers
Exciting New Features in C# 8.0
Exploring Language Improvements in C# 7.2 and 7.3