As experienced programmers, in whatever language we favored before C#, we internalized several practices for developing more efficient code. Sometimes what worked in our previous environment is counterproductive in the .NET environment. This is very true when you try to hand-optimize algorithms for the C# compiler. Your actions often prevent the JIT compiler from more effective optimizations. Your extra work, in the name of performance, actually generates slower code. You're better off writing the clearest code you can create. Let the JIT compiler do the rest. One of the most common examples of premature optimizations causing problems is when you create longer, more complicated functions in the hopes of avoiding function calls. Practices such as hoisting function logic into the bodies of loops actually harm the performance of your .NET applications. It's counterintuitive, so let's go over all the details.
This chapter's introduction contains a simplified discussion of how the JIT compiler performs its work. The .NET runtime invokes the JIT compiler to translate the IL generated by the C# compiler into machine code. This task is amortized across the lifetime of your program's execution. Instead of JITing your entire application when it starts, the CLR invokes the JITer on a function-by-function basis. This minimizes the startup cost to a reasonable level, yet keeps the application from becoming unresponsive later when more code needs to be JITed. Functions that do not ever get called do not get JITed. You can minimize the amount of extraneous code that gets JITed by factoring code into more, smaller functions rather than fewer larger functions. Consider this rather contrived example:
public string BuildMsg( bool takeFirstPath )
{
StringBuilder msg = new StringBuilder( );
if ( takeFirstPath )
{
msg.Append( "A problem occurred." );
msg.Append( "\nThis is a problem." );
msg.Append( "imagine much more text" );
} else
{
msg.Append( "This path is not so bad." );
msg.Append( "\nIt is only a minor inconvenience." );
msg.Append( "Add more detailed diagnostics here." );
}
return msg.ToString( );
}
The first time BuildMsg gets called, both paths are JITed. Only one is needed. But suppose you rewrote the function this way:
public string BuildMsg( bool takeFirstPath )
{
if ( takeFirstPath )
{
return FirstPath( );
} else
{
return SecondPath( );
}
}
Because the body of each clause has been factored into its own function, that function can be JITed on demand rather than the first time BuildMsg is called. Yes, this example is contrived for space, and it won't make much difference. But consider how often you write more extensive examples: an if statement with 20 or more statements in both branches of the if statement. You'll pay to JIT both clauses the first time the function is entered. If one clause is an unlikely error condition, you'll incur a cost that you could easily avoid. Smaller functions mean that the JIT compiler compiles the logic that's needed, not lengthy sequences of code that won't be used immediately. The JIT cost savings multiplies for long switch statements, with the body of each case statement defined inline rather than in separate functions.
Smaller and simpler functions make it easier for the JIT compiler to support enregistration. Enregistration is the process of selecting which local variables can be stored in registers rather than on the stack. Creating fewer local variables gives the JIT compiler a better chance to find the best candidates for enregistration. The simplicity of the control flow also affects how well the JIT compiler can enregister variables. If a function has one loop, that loop variable will likely be enregistered. However, the JIT compiler must make some tough choices about enregistering loop variables when you create a function with several loops. Simpler is better. A smaller function is more likely to contain fewer local variables and make it easier for the JIT compiler to optimize the use of the registers.
The JIT compiler also makes decisions about inlining methods. Inlining means to substitute the body of a function for the function call. Consider this example:
// readonly name property:
private string _name;
public string Name
{
get
{
return _name;
}
}
// access:
string val = Obj.Name;
The body of the property accessor contains fewer instructions than the code necessary to call the function: saving register states, executing method prologue and epilogue code, and storing the function return value. There would be even more work if arguments needed to be pushed on the stack as well. There would be far fewer machine instructions if you were to write this:
string val = Obj._name;
Of course, you would never do that because you know better than to create public data members (see Item 1). The JIT compiler understands your need for both efficiency and elegance, so it inlines the property accessor. The JIT compiler inlines methods when the speed or size benefits (or both) make it advantageous to replace a function call with the body of the called function. The standard does not define the exact rules for inlining, and any implementation could change in the future. Moreover, it's not your responsibility to inline functions. The C# language does not even provide you with a keyword to give a hint to the compiler that a method should be inlined. In fact, the C# compiler does not provide any hints to the JIT compiler regarding inlining. All you can do is ensure that your code is as clear as possible, to make it easier for the JIT compiler to make the best decision possible. The recommendation should be getting familiar by now: Smaller methods are better candidates for inlining. But remember that even small functions that are virtual or that contain try/catch blocks cannot be inlined.
Inlining modifies the principle that code gets JITed when it will be executed. Consider accessing the name property again:
string val = "Default Name";
if ( Obj != null )
val = Obj.Name;
If the JIT compiler inlines the property accessor, it must JIT that code when the containing method is called.
It's not your responsibility to determine the best machine-level representation of your algorithms. The C# compiler and the JIT compiler together do that for you. The C# compiler generates the IL for each method, and the JIT compiler translates that IL into machine code on the destination machine. You should not be too concerned about the exact rules the JIT compiler uses in all cases; those will change over time as better algorithms are developed. Instead, you should be concerned about expressing your algorithms in a manner that makes it easiest for the tools in the environment to do the best job they can. Luckily, those rules are consistent with the rules you already follow for good software-development practices. One more time: smaller and simpler functions
Remember that translating your C# code into machine-executable code is a two-step process. The C# compiler generates IL that gets delivered in assemblies. The JIT compiler generates machine code for each method (or group of methods, when inlining is involved), as needed. Small functions make it much easier for the JIT compiler to amortize that cost. Small functions are also more likely to be candidates for inlining. It's not just smallness: Simpler control flow matters just as much. Fewer control branches inside functions make it easier for the JIT compiler to enregister variables. It's not just good practice to write clearer code; it's how you create more efficient code at runtime.