Welcome!

.NET Authors: Bruce Armstrong, Pat Romanski, Liz McMillan, Yeshim Deniz, Dmitry Sotnikov

Related Topics: .NET

.NET: Article

The Art of Writing High-Performance Code

The Art of Writing High-Performance Code

The Aglo tribe lives on a small island called Zerep, located in the South Pacific. From year to year the Aglos are never sure of what to expect. Sometimes they have plentiful resources and life is good, things run smoothly, and there is much joy and happiness. At other times they find that they can barely find enough to survive on and only their trust in the spirit of the Tij and their natural affinity to do things as efficiently as possible keep them alive. Recently I met with some of the elders of the Aglo tribe and as I sat and listened to their stories, it amazed me how as a software developer I had so much in common with this tiny group of people.

When I started programming we didn't have the luxury of gobs of memory and super-fast processors. It was a dark time when resources were scarce and your livelihood depended on your ability to write highly optimized and efficient code. You needed to consider each and every thing you did and the impact it had on the overall state of the system and its performance. As I visited with the Aglo, I started to realize that just as not much has changed in their world, the reality is that not much has changed in our world of bits and bytes. The reality is that we still need to write highly efficient code and make trade-offs and decisions that in the end create a highly optimized, high-performance system.

In this article we are going to spend some time getting to know some of the dirty details behind writing highly optimized .NET code. We will work primarily with C#, but the edicts learned, the tools uncovered, and the skills gained can be applied to pretty much any of the key .NET languages. We will start by getting comfortable with basic principles related to writing high-performance code, then we will get a handle on how the JIT compiler works to optimize your code so you can work with it and not against it. We will then move on to some basic things you should know regarding coding choices, and we'll wrap up with a review of common tools and tips on how to use them as they relate to performance tuning.

Starting on the Right Foot
The single biggest mistake I come across when asked to help solve performance-related issues is that the developers, architects, and code gnomes did not consider performance from the start. I know that this must seem like a trite and obvious point, but the bottom line is that it is a very common mistake. It is critical that you consider performance implications as part of your design from the very start. This is especially true when dealing with distributed resources, such as systems that need to access resources across a network or contend for resources that are synchronous in nature. So from the very beginning, consider the implications of your design, the data structures you employ, the looping mechanisms, how you reference methods and data, and how you access resources. As you architect solutions and write code, you need to consider what is happening under the covers. How well do you understand the .NET internals and their impact on the performance of your code? Let's try a pop quiz and see how you do.

Let's say that you write the following code in VB.NET; what happens under the covers?

Dim HourlyPay
HourlyPay = 5

Since you declared it without specifying a type, the system defaults to type object - that is the first important thing to note. You have created the variable "Hourly Pay" as an object, which means you get all the cool overhead of an object when all you really wanted was an integer. But wait, there's more! Let's say you want to give the lucky chap making five dollars an hour an extra two bucks, so you write the following code:

Dim HourlyPay
Dim RaisePay as Integer
Dim TotalPay
HourlyPay = 5
RaisePay = 2
TotalPay = HourlyPay + RaisePay

You would end up with a series of boxing and unboxing operations, and what's worse, the compiler would need to make calls across assemblies to deal with adding an object ("HourlyPay") to an integer ("RaisePay"). If you had simply declared "HourlyPay" as an integer to start with, you would have avoided the cross-assembly calls and the boxing/unboxing operations, thereby increasing performance. The obvious lesson here is that you should declare variables in VB.NET specifically. But the more important lesson is that you should get to know the .NET internals so that you are aware of the implications of what happens under the covers. One key technique you can employ, especially if you are troubleshooting performance problems, is to examine the IDL generated by your language compiler. When you examine the IDL, look for areas that contain the "box" directive, such as:

IL_0001: box [mscorlib] System.Int32

You should also look for cross-assembly calls. If possible, rework your code to avoid the boxing operations. You should also consider ways to avoid going across assemblies. When you call across assemblies, the JIT compiler tends to avoid inlining the method. Speaking of the JIT compiler, just how much should we rely on the JIT compiler to optimize our code?

JIT-Compiler Optimizations
Let's begin examining the preceding question by getting a sense of what the JIT knows when it is optimizing your code. The JIT knows what type of processor it is running on and it knows the location of dependent assemblies in memory as well as other runtime information. Compare this to a traditional compiler that - although it can provide some optimization - has to generalize, since it does not have any insight as to what your runtime environment will be like when your code is executed. Although you may take a hit on the initial compilation of your code when your application starts, there can be a rather strong argument made that the code produced by the JIT compiler is more efficient than code generated by a traditional compiler. The nature of the optimizations and how they are performed go beyond an introductory article, but so you have a basic sense of what the JIT does, see Table 1.

An interesting point to keep in mind while you consider the compilation of your code is the use of the NGEN (Native Image Generator). This utility allows you to precompile your code, and often people believe that using NGEN will help speed up code execution since you avoid the JIT process during runtime. But think back to what I said about the JIT compiler and what it knows about the runtime environment. If you use NGEN, you will not have the benefit of understanding the environment in which your code will run. Hence, you lose many of the benefits gained when using the JIT compiler at runtime, so the reality is that you may end up with code that runs less efficiently.

Maintain a Holistic Approach
Now that we have an understanding of what the JIT compiler can do to aggressively aid in the optimization of our code, what can we do to make sure we don't trip up the JIT compiler? A lot of it comes down to understanding the semantics of the language you are programming in and how that language deals with the CLR. For instance, in our VB.NET example we learned that not being specific about how we declare a variable can lead to performance penalties. In Table 2 I have provided some suggestions on things you should consider when writing code, but it is important that you take a more holistic view of your application if you are going to develop a truly high-performance application.

When we utilize a "holistic approach" it means we need to examine the whole system and its interdependencies, as opposed to focusing on one specific area. This does not mean that after profiling your code and working with some of the tools we will discuss shortly that you shouldn't turn your attention to a specific problematic piece of code. But it is important that you consider the various aspects of your code and how each piece contributes to overall performance. For instance, in Table 2 I outline specific items related to core .NET development, but make no mention of ADO.NET, IIS, SQL, XML, socket programming, threading, or other key aspects of an application that contribute to your ability to successfully write high-performance code. Consider and profile your data access routines; consider the impact of establishing network connections, the pros and cons of threading, and issues related to object management. If we were to create a pattern or rule of thumb, it would probably look like something in Table 3. Feel free to adjust those rules as you see fit, but remember that regardless of how you slice it, you need to consider all the aspects of the system.

Developing a Performance Toolkit
Once you have written your application - hopefully with performance in mind from the start - you should consider how to profile, analyze, and measure your application's performance. In this section we will review some ways to create a performance toolkit of sorts. We will examine some common tools, learn how to use performance counters, and examine some simple ways to invoke and use timers.

Many of your best performance tools are also the easiest to use. The Windows Task Manager and Performance Monitor provide you with a wealth of information that you can use to analyze the performance of your system. Using the Task Manager is a quick way to begin collecting data and analyzing application performance. If you click on "View" and then "Select Columns" you can add a variety of counters that can provide a wealth of feedback. Pay particular attention to how your application is using memory and the CPU.

The Performance Monitor is another great way to learn what your application is doing under the covers. .NET installs a set of counters that help you monitor exceptions, the JIT, threading, memory, networking, and much more. One very useful aid in analyzing performance is the development of your own performance counters. Developing your own performance counters not only helps optimize your code, but can also help you develop a proactive means by which to avoid or identify performance problems once your code is deployed. Let's take a look at the key steps required to develop your own performance counters using .NET. I am going to use C# in this example, but the approach is virtually the same for VB.NET. Although Listing 1, which is available for download from www.sys-con.com/dotnet/sourcec.cfm, is rather self-explanatory, the basic concept is that you create a Counter category using:

PerformanceCounterCategory.Create(...);

You then create an instance of the category:

pcTotalLoops = new PerformanceCounter(...);

Using your own counters in your application can help you better find troublesome performance hotspots. It is also a great way to get a handle on application performance in a production environment using standard tools and techniques that customers and peers can readily access.

Wrapping Up
As you can see, there is a lot to grasp when it comes to understanding the art of writing high-performance code. But the bottom line is that it really isn't all that hard to develop systems that perform extremely well using .NET. By using the key concepts presented in this article you should have a good basis to develop high-performance code. I have presented some basic information on the JIT compiler and its optimization services, reviewed the idea behind a holistic approach to writing high-performance code, and also walked through the use of some tools you can use to analyze your code, as well as how to create your own performance counters as a way to provide statistics you can use to analyze performance. Hopefully, I have provided you with a good foundation and removed some of the mystery and complexity associated with writing high-performance .NET code.

By the way, for those of you who are geography buffs, there is no Zerep Island or tribe of magical Aglo. But even though they don't exist, the moral of the story should be taken to heart - and that is that you need to be very considerate of the resources in your environment. Develop and write code that is able to react to situations in which at one moment you have plenty of resources and the next moment they are scarce. If you apply the techniques and approaches in this article, I am confident you will have no problem dealing with the bounty or scarcity of resources on your application's digital island.

More Stories By John Gomez

John Gomez, open source editor for .NET Developer's Journal, has over 25 years of software development and architectural experience, and is considered a leader in the design of highly distributed transaction systems. His interests include chaos- and fuzzy-based systems, self-healing and self-reliant systems, and offensive security technologies, as well as artificial intelligence. John started developing software at age 9 and is currently the CTO of Eclipsys Corporation, a worldwide leader in hospital and physician information systems.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.