Analyze IL and say hello to your performance





2.00/5 (8 votes)
Sep 4, 2004
3 min read

50181
Avoid unnecessary variable declaration and redundant instructions. Analyzing and understanding the compiler and generated IL instructions in a simple example, saving your performance.
Introduction
In this article, I’ll try to demonstrate a theory which I have since some time ago, and that some people to whom I talk sometimes ignore. The “please-avoid-too-many-and-unnecessary-private –variables-when-you-really-don’t-need it!” theory, or friendly name “The-Not-Clearer-Code” theory.
First of all, I'm not English natural and I’m not an expert in .NET, so, please be gentle with me :)
The problem
Imagine the following code:
int result = Sum(value1,value2);
if(result>0)
return true;
else
return false;
Most of you already know what I mean. Why the hell do we need result
. Yes, why do we need it? We don’t. But some guys insist on doing this. Usually, the justification is “It’s clearer to do an if
instruction next, and more readable”. I know… you are not one of them ;). But for those who are not reading this article, I’m going to definitely explain why you should avoid unnecessary code, and why you should turn that on a “return Sum(value1, value0)>0;
” or even better, on a “return value1+value0>0;
”.
Of course, this article is not about the “exact” code as shown, but about avoiding unnecessary stack memory use, and really understanding why.
So what are really the reasons?
First of all, no, sorry, but it’s not clearer! (Anybody disagrees?). I cannot understand why someone will find this clearer. If someone who is not reading this article thinks that, I suggest that also refactor the variable name result
to resultOfSomeSomeBetweenTwoValuesThatImGonnaSeeIfItIsGreatherThanZero
. That is clearer.
But after all, does somebody know what this is going to result in your code? Of course, you know: unnecessary stack memory consumption and redundant runtime instructions.
Please consider the following console app:
using System;
namespace TestStack
{
class Class1
{
private static int Sum(int value1, int value2)
{
return value1 + value2;
}
private static bool AnnoyingIsResultGreaterThanZero
(int value1, int value2 )
{
int result = Sum(value1,value2);
if (result>0)
return true;
else
return false;
}
private static bool IsResultGreaterThanZero(int value1, int value2 )
{
return Sum(value1,value2)>0;
}
[STAThread]
static void Main(string[] args)
{
const int ITERATIONS = 2147483647;
Console.WriteLine("Doing {0} iterations...",ITERATIONS);
System.DateTime start ;
bool result;
//Warm up
result = IsResultGreaterThanZero(1,1);
result = AnnoyingIsResultGreaterThanZero(1,1);
result = (Sum(1,1)>0);
result = (1+1>0);
start = System.DateTime.UtcNow;
Console.WriteLine("Calling the " +
"AnnoyingIsResultGreaterThanZero method...");
for (int i=0 ; i<ITERATIONS ; i++)
{
result = AnnoyingIsResultGreaterThanZero(1,1);
}
Console.WriteLine("Total time taken with " +
"AnnoyingIsResultGreaterThanZero method : " +
(System.DateTime.UtcNow - start).ToString());
start = System.DateTime.UtcNow;
Console.WriteLine("Calling the IsResultGreaterThanZero method...");
for (int i=0 ; i<ITERATIONS ; i++)
{
result = IsResultGreaterThanZero(1,1);
}
Console.WriteLine("Total time taken with " +
"IsResultGreaterThanZero method : " +
(System.DateTime.UtcNow - start).ToString());
start = System.DateTime.UtcNow;
Console.WriteLine("Directly calling the Sum method ...");
for (int i=0 ; i<ITERATIONS ; i++)
{
result = (Sum(1,1)>0);
}
Console.WriteLine("Total time taken with Sum method: " +
(System.DateTime.UtcNow - start).ToString());
start = System.DateTime.UtcNow;
Console.WriteLine("Directly making the sum ...");
for (int i=0 ; i<ITERATIONS ; i++)
{
result = (1+1>0);
}
Console.WriteLine("Total time taken with directly sum : " +
(System.DateTime.UtcNow - start).ToString());
Console.ReadLine();
}
}
}
We have some basic members here:
Sum
returns the sum between two integers,AnnoyingIsResultGreaterThanZero
andIsResultGreaterThanZero
returntrue
if the result between two values is greater than zero, butAnnoyingIsResultGreaterThanZero
will do it in a redundant way;Main
starts the app and runs some perf tests, writing the time taken to the console;
Both AnnoyingIsResultGreaterThanZero
and IsResultGreaterThanZero
produce same results. Although, both are radically different in the generated IL.
Let’s see the IL generated by IsResultGreaterThanZero
. To see the IL, you can use reflector or ILDASM provided in the SDK. (Tip: with reflector, you can see more details about the generated IL when the mouse is over the IL instruction.)
.method private hidebysig static bool
IsResultGreaterThanZero(int32 value1, int32 value2) cil managed
{
// Code Size: 11 byte(s)
.maxstack 2
L_0000: ldarg.0
L_0001: ldarg.1
L_0002: call int32 TestStack.Class1::Sum(int32, int32)
L_0007: ldc.i4.0
L_0008: cgt
L_000a: ret
}
This seems very simple IL. L_0000
and L_00001
load arguments to the evaluation stack. Next, we call Sum
, load a 0 (the value to compare), and compare the value returned by Sum
and the value just pushed to stack (0), and exits the sub.
When analyzing the IL generated by AnnoyingIsResultGreaterThanZero
, we can see the following:
.method private hidebysig static bool
AnnoyingIsResultGreaterThanZero(int32 value1, int32 value2) cil managed
{
// Code Size: 28 byte(s)
.maxstack 4
.locals (
int32 V_0,
bool V_1)
L_0000: ldarg.0
L_0001: ldarg.1
L_0002: call int32 TestStack.Class1::Sum(int32, int32)
L_0007: stloc.0
L_0008: ldloc.0
L_0009: ldc.i4.0
L_000a: ble.s L_0013
L_000c: ldc.i4.1
L_000d: stloc.1
L_000e: br L_001a
L_0013: ldc.i4.0
L_0014: stloc.1
L_0015: br L_001a
L_001a: ldloc.1
L_001b: ret
}
As you can see, this is radically different. First of all, and as you can see, with line L_000a
and L_000d
, we’ll need more memory to store the result of “if
” evaluation. Second, the runtime has 10 more instructions to evaluate.
Running the app and seeing results
My test environment is a PIV 3.06Mhz HT, 1GB RAM, running .NET 1.1. The app was compiled in release without /optimize switch. The results are:
Doing 2147483647 iterations...
Calling the AnnoyingIsResultGreaterThanZero method...
Total time taken with AnnoyingIsResultGreaterThanZero method : 00:00:11.5937500
Calling the IsResultGreaterThanZero method...
Total time taken with IsResultGreaterThanZero method : 00:00:01.5468750
Directly calling the Sum method ...
Total time taken with Sum method: 00:00:01.5312500
Directly making the sum ...
Total time taken with directly sum : 00:00:01.7187500
I'm making 2147483647 iterations. I know it's a lot, but with this test case, only with a lot of iterations we get some conclusions. As you can see, we're talking of 10 times more.
Even using /optimize switch, we still get slower results as you may see:
Doing 2147483647 iterations...
Calling the
AnnoyingIsResultGreaterThanZero method...
Total time taken with AnnoyingIsResultGreaterThanZero method : 00:00:02.2343750
Calling the IsResultGreaterThanZero method...
Total time taken with IsResultGreaterThanZero method : 00:00:01.4843750
Directly calling the Sum method ...
Total time taken with Sum method: 00:00:01.5000000
Directly making the sum ...
Total time taken with directly sum : 00:00:01.5000000
Conclusion
This is a very risky subject for me, since, as I told in previous article comments, I'm not an expert in JIT. In the previous article version, I committed some mistakes exactly because of that, and maybe I'm having worst mistakes right now . . . :) Please feel free to comment and help to take conclusions on this subject.