Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Effective C# - Performance notes

0.00/5 (No votes)
4 Jul 2005 1  
Don't emphasize practices that may have an affect on performance in a few cases

Introduction

The world is full of advice that sounds reasonable, but really isn't. Too often, the justification for a piece of advice is lost in the multiple retellings that cause it to enter our common practice. While we continue to repeat something as truth, the reasons are lost forever.  We've all heard many of these bits of advice over and over: "Look both ways before crossing the street", "Don't run with scissors", you get the idea.  The same is true for technical advice. It's easy to simply repeat what you've heard before until it becomes unquestionably accepted.

That's wrong (although I don't recommend running with scissors).

All advice is based on some real experience, including the justification of why that advice should be followed. When all you know is the advice, you can't spot the exceptions, or recognize when a particular recommendation has become obsolete.

This brings me to the charter of the Effective Software Development Series and my own contribution, Effective C#. Scott Meyer's advice to me was to focus on the advice that experienced C# developers give to other C# developers. Yet even though Effective books include the most useful items for practicing developers, few recommendations are universally correct. That's why Effective books go into detail on the justification of each item. That justification gives the reader the knowledge needed to follow or ignore any individual item. Many useful pieces of advice have more than one simple justification.  A good example is the recommendations on the ‘as' and ‘is' keywords instead of casting.  It's true that those keywords let you test runtime type information without writing try / catch blocks or having exceptions thrown from your methods.  It's also true that there are times when throwing an exception is the proper behavior when you find an unexpected type.  But the performance overhead of exceptions is not the whole story with these two operators.  The as and is operators perform run time type checking, ignoring any user defined conversion operators. The type checking operators behave differently than casts. You'll find similar subtleties in every item in Effective C#.  By examining all those details at least once, you'll better understand the justification of each guideline, and those occasions when you should ignore a particular recommendation.

Even though the previous paragraph mentions runtime performance as one justification for choosing among different language constructs, it's worth noting that very few Effective Items are justified strictly based on performance. The simple fact is that low-level optimizations aren't going to be universal. Low level language constructs will exhibit different performance characteristics between compiler versions, or in different usage scenarios.  In short, low-level optimizations should not be performed without profiling and testing.  However, there are design-level guidelines that can have a major impact on the performance of a program.  Item 34 in Effective C# discusses some guidelines for the granularity of web service APIs.  These kinds of design issues can have a large impact on the performance of service oriented application.  All of chapter 2 in Effective C# discusses resource management.  This includes IDisposable and finalizers, and efficient class and object initialization.  But those guidelines are not strictly for performance; they include strategies to ensure that your applications are robust and correct, and more easily extended over time.

The remainder of this article will examine a few common recommendations you may have heard. I'll point out when those items are likely valid, and when following them can get you in deep trouble. I'll start with the only universal recommendation in this article:

The one universal recommendation: Optimization means Profiling.

This is a special case of the best practice that you actually test code changes.  When you fix a bug, you follow an accepted set of steps: You verify that the bug exists by testing. You make the change. You verify that the bug is fixed by testing (You should also perform reasonable regression testing to make sure you didn't break anything else.) The same process is necessary if you claim to be optimizing code. You must measure the performance before making the change, make the change, and then measure the performance again. No matter whose recommendation you are following, you need to do these steps. Anything less is simply not engineering.

Conventional Wisdom: String Concatenation is expensive

You've undoubted heard that string concatenation is an expensive operation. Whenever you write code that appears to modify the contents of a string, you are actually creating a new string object and leaving the old string object as garbage. I used the following example in Effective C# (Item 16):

string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString();

Is just as inefficient as if you had written:

string msg = "Hello, ";
string tmp1 = new String( msg + thisUser.Name );
string msg = tmp1; // "Hello " is garbage.
string tmp2 = new String( msg + ". Today is " );
msg = tmp2;        // "Hello <user>" is garbage.
string tmp3 = new String( msg + DateTime.Now.ToString( ) );
msg = tmp3;        // "Hello <user>. Today is " is garbage. 

The strings tmp1, tmp2, and tmp3, and the originally constructed msg ("Hello") are all garbage. The += method on the string class creates a new string object and returns that string. It does not modify the existing string by concatenating the characters to the original storage. For simple constructs like the one above, you should use the string.Format() method:

string msg = string.Format ( "Hello, {0}. Today is {1}", 
                             thisUser.Name, DateTime.Now.ToString( ));

For more complicated string operations you can use the StringBuilder class:

StringBuilder msg = new StringBuilder( "Hello, " );
msg.Append( thisUser.Name );
msg.Append( ". Today is " );
msg.Append( DateTime.Now.ToString());
string finalMsg = msg.ToString();

In the end, you have two choices to avoid churning through string objects: string.Format, and the StringBuilder class. I choose based on readability.

Conventional Wisdom Debunked: Checking the Length property is faster than an equality comparison

Some people have commented that this idiom:

if ( str.Length != 0 )

is preferable to this one:

if ( str != "" )

The normal justification is speed. People will tell you that checking the length of the string is faster than checking to see if two string are equal. That may be true, but I really doubt you'll see any measurable performance improvement. Both involve one function call, and both are likely optimized well by the .NET Framework team. My one personal preference is to use the String.Empty constant for these comparisons because I think it is more readable:

if ( str != string.Empty )

Conventional Wisdom Investigated: String.Equal is better than ==

I'll start by referring you to Item 9 in Effective C#. I spend several pages discussing all the gory details about different methods of doing equality comparisons in .NET. Here, I'll point to a few rules to consider. The most important one is this:  for the string class, Operator == and String.Equal() will always give the same result. Period. You can use either interchangeably without affecting the behavior of your program. Which you pick will depend on your own personal style, and the compile-time types you're working with.

The == operator is strongly typed, and will only compile if both parameters are strings. That implies that there are no casts or conversions inside its implementation.  String.Equal, on the other hand, has multiple overloads.  There is a strongly-typed version, and another version that overrides the System.Object.Equals() method. If you write code that calls the override of the Object version, you will pay a small performance penalty. That penalty comes because there is a (slight) performance hit for the virtual function call, and another (slight) performance hit for the conversion. Of course, if either operand has the compile-time type of object, you'll pay for the conversion costs anyway, so it doesn't much matter.

The bottom line on equality and strings is that if you use the method that matches the compile time types you're working with, you will certainly get the expected behavior, and you'll likely get the best performance. If you want all the details, see Item 9 in Effective C#.

Conventional Wisdom: Boxing and Unboxing are bad

This is true, boxing and unboxing are often associated with negative behaviors in your program. Too many times, the justification is performance. Yes, boxing and unboxing cause performance issues. Sometimes though, creating your own collection classes or resorting to arrays will be just as bad. It depends on how often you access the values in the collection. Just once?  Then use the ArrayList class. You'll probably come out fine.  Several hundred times?  Think about a custom collection. Several hundred times, but you're also changing the size of the collection several hundred times?  I've got no idea which is better, which is why you profile.

More important than performance is correctness. I showed two different examples in Effective C# (pp 106-107) where boxing and unboxing can cause incorrect behavior in your application. Looking for boxing and unboxing is tricky because the compiler won't help you, and it does occur in multiple places. Everyone has seen how the collection classes will cause boxing and unboxing to occur. But, the same effect happens when you access a value type through an interface and when you pass values to methods that expect System.Object. For example, this line boxes and unboxes three times:

Console.WriteLine("A few numbers:{0}, {1}, {2}",  25, 32, 50);

Boxing and unboxing are to be avoided, but that's not the same as saying you should avoid using the Systems.Collections classes for values. There are more ways to box values, and sometimes the code savings is worth the performance costs.

Conventional Wisdom Explained: ‘as' and ‘is' vs. casting

I'm not going to discuss performance here, because that's not the reason to pick one or the other. The as and is operators do not examine user defined conversion operators.  Casts can make use of both explicit and implicit conversion operators.  The exact behavior of user defined conversions and casts gets complicated, and relies on the compile-time type of the arguments.  I discussed all the details on pages 18 – 24 of Effective C# (Item 3).

The next important question is whether or not it's an error condition if the conversion fails. If it's an expected behavior to receive inputs that are not the expected type, the as and is operators provide simpler checks than introducing exceptions. However, if you're going to write this:

if ( ! o is ExpectedType )
  throw new ArgumentTypeException( "You didn't use the right type" );

Please just use the cast instead. It's clearer. On a related note, consider the difference between double.Parse() and double.TryParse(). One assumes success and throws exceptions when it fails. The other returns error codes to indicate a problem.

Conventional Wisdom Debunked: for loops are faster than foreach

I'm not going to say that foreach is faster. However, I am going to say I'm happy to work in an environment where the compiler writers work hard to optimize the language constructs that are the most versatile. The C# 1.1 compiler added optimizations to the code generated by foreach loops and arrays. In version 1.0, the for loops were much faster. That's no longer true. (Remember that earlier recommendation to profile your code?)

The best practice here is to write the clearest code you can, and expect the compiler to optimize properly. If necessary, profile and improve.

Conventional Wisdom Debunked: NGen will improve performance

Maybe. Then again, maybe not.

This is a dangerous recommendation.  Yes, it's true that nGen will lower the startup time for some assemblies.  It's also true that you will lose some of the advantages of JIT compilation.  There are a number of optimizations that the JIT compiler can perform only at runtime.  This whitepaper gives a good overview of the advantages of leaving the JIT step to runtime.

This is a performance only recommendation that I would never blindly follow without profiling both versions, both startup times and operating times.  In addition, you should profile combinations of NGen'ed and regular assemblies.

Conclusion

As I said in my opening the purpose of an Effective book is to provide the most useful pieces of advice to the most developers.  That advice must be justified with the technical information to help readers decide when advice applies, and under what scenarios it can be safely ignored.  I've given you a taste of the justification I gave readers in Effective C#, and how that sometimes conflicts with the recommendations often repeated among C# developers.  Addison-Wesley, The Code Project and I are working to excerpt some of the Effective C# items here for Code Project readers.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here