General Guidelines for Optimising .NET Applications
Martin Bell
Boxing and Unboxing
Boxing is the extra processing the common language runtime must do when you treat a value type as a reference type. Boxing is necessary, for example, if you declare an Integer variable and then assign it to an Object variable or pass it to a procedure that takes an Object argument. In this case, the common language runtime must box the variable to convert it to type Object. It copies the variable, embeds the copy in a newly allocated object, and stores its type information.
If you subsequently assign the boxed variable to a variable declared as a value type, the common language runtime must unbox it, that is, copy the data from the heap instance into the value type variable. Furthermore, the boxed variable must be managed on the heap, whether or not it is ever unboxed.
Boxing and unboxing cause very significant performance degradation. If your application frequently treats a value type variable as an object, it is better to initially declare it as a reference type. An alternative is to box the variable once, retain the boxed version as long as it is being used, and then unbox it when the value type is needed again.
String manipulation
String concatenation is a common task in many programming situations, however due to the fact that .NET strings are immutable (every time you change a string variable you effectively destroy the existing object and create a new), this can cause a high memory and performance overhead if you manipulate the string many times.
The StringBuilder class in the System.Text namespace supports a mutable string. For large amounts of string concatenations this classes' Append function proves to be many times faster than a standard string's + operator.
Exception handling
The exception handling classes in the .NET Framework are a very powerful mechanism for creating robust applications. However, overuse of exception handling can lead to performance problems. Try to minimise the number of loops inside and Try block and minimise the number of Try blocks inside a loop.
Function sizes
The size of functions can greatly affect the performance of .NET applications, particularly when functions are called accross application boundaries using COM+ or .NET remoting. The general rule is that functions should not be too small and over-frequently called (nicknamed chatty functions) because of the overhead associated with invoking the functions and marshalling of parameter data. For distributed objects, try to favour function calls that initialise a number of fields at once, rather than property accessors. In addition to this, try to favour small 'by value' parameters rather that 'by reference' parameters.
Conversely, functions should not be too big. Function less than 1000 lines long benefit from just-in-time compiling. The JIT compiler can perform numerous opimisations on smaller functions including the generation of inline code which boosts performance.
Object oriented design
Try to avoid the 'over-cooking' of object oriented frameworks within your applications. Try to keep them as simple as possible and reduce the amount of inheritence and use of virtual function calls which take approximately twice as much time to call as non-virtual methods.
Data access
Use the specific ADO.NET objects for the databases that you are accessing. For example, if you are working with Microsoft SQL Server, ensure that you use the classes defined in the System.Data.SqlClient namespace. For more performance tips using ADO.NET, see our Data Access article.
It is also more efficient to create and destroy database connections at the time you need them, rather than maintaining persistent connections. This reduces memory overhead, particularly in enterprise applications and makes use of the .NET framework's built-in pooling mechanism.