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;
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
and IsResultGreaterThanZero
return true
if the result between two values is greater than zero, but AnnoyingIsResultGreaterThanZero
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
{
.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
{
.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.