EDIT 3
If I go back to the original test with the timing error fixed, I get output similar to this.
Conditional took 67ms
Normal took 83ms
Cached took 73ms
Which shows that the Ternary/Conditional operator can be marginally faster in a for
loop. Given the previous finding, that when the logical branch is abstracted out of the loop, the if
block beats the Ternary/Conditional operator, we can infer that compiler is able to make additional optimizations when the Conditional/Ternary operator is used iteratively, in at least some cases.
It is not clear to me why these optimizations do not apply or, are not applied, to the standard if
block. The actual differential is fairly minor and, I argue, a moot point.
EDIT 2
There is infact a glaring error in my test code highlighted here
The Stopwatch
is not reset between calls, when I use Stopwatch.Restart
instead of Stopwatch.Start
and up the iterations to 1000000000, I get the results
Conditional took 22404ms
Normal took 21403ms
This is more like the result I was expecting and borne out by the extracted CIL. So the "normal" if
is in fact marginally faster then the Ternary\Conditional operator, when isolated from surrounding code.
EDIT
After my investigations outlined below, I would suggest that when using a logical condition to choose between two constants or literals the Conditional/Ternary operator can be significantly faster than the standard if
block. In my tests, it was roughly twice as fast.
However I can't quite work out why. The CIL produced by the normal if
is longer but for both functions the average execution path seems to be six lines, including 3 loads and 1 or 2 jumps, any ideas?.
Using this code,
using System.Diagnostics;
class Program
{
static void Main()
{
var stopwatch = new Stopwatch();
var conditional = Conditional(10);
var normal = Normal(10);
var cached = Cached(10);
if (new[] { conditional, normal }.Any(x => x != cached))
{
throw new Exception();
}
stopwatch.Start();
conditional = Conditional(10000000);
stopWatch.Stop();
Console.WriteLine(
"Conditional took {0}ms",
stopwatch.ElapsedMilliseconds);
////stopwatch.Start(); incorrect
stopwatch.Restart();
normal = Normal(10000000);
stopWatch.Stop();
Console.WriteLine(
"Normal took {0}ms",
stopwatch.ElapsedMilliseconds);
////stopwatch.Start(); incorrect
stopwatch.Restart();
cached = Cached(10000000);
stopWatch.Stop();
Console.WriteLine(
"Cached took {0}ms",
stopwatch.ElapsedMilliseconds);
if (new[] { conditional, normal }.Any(x => x != cached))
{
throw new Exception();
}
Console.ReadKey();
}
static int Conditional(int iterations)
{
var ret = 0;
for (int j = 0; j < iterations; j++)
{
ret = (j * 11 / 3 % 5) + (ret % 11 == 4 ? 2 : 1);
}
return ret;
}
static int Normal(int iterations)
{
var ret = 0;
for (int j = 0; j < iterations; j++)
{
if (ret % 11 == 4)
{
ret = 2 + (j * 11 / 3 % 5);
}
else
{
ret = 1 + (j * 11 / 3 % 5);
}
}
return ret;
}
static int Cached(int iterations)
{
var ret = 0;
for (int j = 0; j < iterations; j++)
{
var tmp = j * 11 / 3 % 5;
if (ret % 11 == 4)
{
ret = 2 + tmp;
}
else
{
ret = 1 + tmp;
}
}
return ret;
}
}
Compiled in x64 Release Mode, with optimizations, run without a debugger attached. I get this output,
Conditional took 65ms
Normal took 148ms
Cached took 217ms
and no exception is thrown.
Using ILDASM to disassemble the code I can confirm that CIL for the three methods differs, the code for Conditional
method being somewhat shorter.
To really answer the "why" question, I would need to understand the code of the compiler. I would probably need to know why the compiler was written that way.
You can break this down even further, so that you actually compare just logical functions and ignore all other activity.
static int Conditional(bool condition, int value)
{
return value + (condition ? 2 : 1);
}
static int Normal(bool condition, int value)
{
if (condition)
{
return 2 + value;
}
return 1 + value;
}
Which you could iterate with
static int Looper(int iterations, Func<bool, int, int> operation)
{
var ret = 0;
for (var j = 0; j < iterations; j++)
{
var condition = ret % 11 == 4;
var value = ((j * 11) / 3) % 5;
ret = operation(condition, value);
}
}
This tests still show a performance differential but, now the other way, simplified IL below.
... Conditional ...
{
: ldarg.1 // push second arg
: ldarg.0 // push first arg
: brtrue.s T // if first arg is true jump to T
: ldc.i4.1 // push int32(1)
: br.s F // jump to F
T: ldc.i4.2 // push int32(2)
F: add // add either 1 or 2 to second arg
: ret // return result
}
... Normal ...
{
: ldarg.0 // push first arg
: brfalse.s F // if first arg is false jump to F
: ldc.i4.2 // push int32(2)
: ldarg.1 // push second arg
: add // add second arg to 2
: ret // return result
F: ldc.i4.1 // push int32(1)
: ldarg.1 // push second arg
: add // add second arg to 1
: ret // return result
}