2

I'm just trying to get into IL because I'm working with code-injection. I'm required to analyze code and cover various cases.

Sadly it doesn't work to inject a method call at the end if the last instructions are inside an if-clause, because the call is being contained by the paranthesis then.

Now I've been analyzing if-code being translated into IL and I'm a bit confused by how this is done. Obviously the compiler reverses the if. Is this because of performance reasons? If so by how far does this improve performance?

See for yourself:

        string test;
        Random rnd = new Random();
        bool b = rnd.Next(0, 10) == 3;
        if (b)
        {
            // TRUE
            test = "True branch";
            // END TRUE
        }
        else
        {
            // FALSE
            test = "False branch";
            //END FALSE
        }

and this is the output:

    IL_0000: nop
    IL_0001: newobj instance void [mscorlib]System.Random::.ctor()
    IL_0006: stloc.1
    IL_0007: ldloc.1
    IL_0008: ldc.i4.0
    IL_0009: ldc.i4.s 10
    IL_000b: callvirt instance int32 [mscorlib]System.Random::Next(int32,  int32)
    IL_0010: ldc.i4.3
    IL_0011: ceq
    IL_0013: stloc.2
    IL_0014: ldloc.2
    IL_0015: ldc.i4.0
    IL_0016: ceq
    IL_0018: stloc.3
    IL_0019: ldloc.3
    IL_001a: brtrue.s IL_0026

    IL_001c: nop
    IL_001d: ldstr "True branch"
    IL_0022: stloc.0
    IL_0023: nop
    IL_0024: br.s IL_002e

    IL_0026: nop
    IL_0027: ldstr "False branch"
    IL_002c: stloc.0
    IL_002d: nop

    IL_002e: ret

As you can see, after the comparison of the Random result with the const 3 it does a comparision against 0 again and thus reverses the result which is equivalent to if (false).

What reason has this? Isn't it less performant since you need additional instructions? Does this happen always?

SharpShade
  • 1,761
  • 2
  • 32
  • 45
  • You are looking at the debug version. Change it to the release version and it uses a `brfalse.s` – xanatos Mar 10 '15 at 13:07
  • There is no `cne`, which may be why `ceq` is used; but then the question becomes why `brfalse` instead of `brtrue` in RELEASE. Both compare a flag and would JIT down to a single instruction in almost all CPU instructions sets. In which case, it was probably just a choice someone made at some point to have one block of code to generate a single comparison type instead of code to choose between two different comparison types. I do not believe there is an answer to this question. – Peter Ritchie Mar 10 '15 at 13:30
  • @Peter Technically the Debug version adds four instructions and an additional stack location IL_0015, IL_0016, IL_0018, IL_0019 and stack location .3, but being Debug IL instructions I think this isn't a real problem. While JITting they can be pattern-matched and removed – xanatos Mar 10 '15 at 13:32
  • @xanatos exactly; so why compare against equality instead of inequality (or compare against 0/false over !0/true)? – Peter Ritchie Mar 10 '15 at 13:34

2 Answers2

1

You are looking at the debug version. Change it to the release version and it uses a brfalse.s

IL_0000: newobj instance void [mscorlib]System.Random::.ctor()
IL_0005: stloc.1
IL_0006: ldloc.1
IL_0007: ldc.i4.0
IL_0008: ldc.i4.s 10
IL_000a: callvirt instance int32 [mscorlib]System.Random::Next(int32, int32)
IL_000f: ldc.i4.3
IL_0010: ceq
IL_0012: stloc.2
IL_0013: ldloc.2
IL_0014: brfalse.s IL_001e

IL_0016: ldstr "True branch"
IL_001b: stloc.0
IL_001c: br.s IL_0024

IL_001e: ldstr "False branch"
IL_0023: stloc.0

I added a Console.WriteLine, otherwise the test variable was removed.

IL_0024: ldloc.0
IL_0025: call void [mscorlib]System.Console::WriteLine(string)
IL_002a: ret

So the differences between Debug and release are:

// Debug
IL_0015: ldc.i4.0
IL_0016: ceq
IL_0018: stloc.3
IL_0019: ldloc.3
IL_001a: brtrue.s IL_0026

vs

// Release
IL_0014: brfalse.s IL_001e

So four additional instructions for the Debug version, and an "inverse" if.

First I will say that the C# compiler tries to keep the code in the same order that it's written. So first the "true" branch, then the "false" branch.

Ok... I'm doing an hypotesis...

Let's say that the problem is in Debug mode... In Debug mode the code must be verbose... very verbose. So

if (b)

is translated to

if (b == true)

sadly true is "anything-but-0", so it's easier to write

if (!(b == false))

because false is "0". But that is what is written in Debug mode :-) Only Debug mode uses a temp variable

as

// bool temp = b == false;
IL_0015: ldc.i4.0
IL_0016: ceq
IL_0018: stloc.3
IL_0019: ldloc.3

and

// if (temp) // go to else branch
IL_001a: brtrue.s IL_0026
xanatos
  • 109,618
  • 12
  • 197
  • 280
  • The code in the debug branch is in the same order as written. – David Heffernan Mar 10 '15 at 13:12
  • 1
    The real difference here is that the boolean variable has been optimised away. That's the only reason for debug using brtrue and release using brfalse. If you use release build code on an if with a boolean that cannot be optimised away then I predict you will see compare against zero followed by brtrue. – David Heffernan Mar 10 '15 at 13:15
  • @Peter How could it have done that? There is no cne. – David Heffernan Mar 10 '15 at 13:36
0

The compiler hasn't reversed anything. Note that the two branches of the if statement appear in the same order in the IL as they do in your source code. You can see that from the order of the two strings.

The use of brtrue is simply the natural IL for branching when a boolean is false. Testing a boolean for truth means comparing it against 0. A value of 0 is false, anything else is considered to be true.

So, the compiler emits IL to compare against 0. If that comparison is true, that is if the boolean has ordinal value of 0, then the boolean is false. So, branch if equal to zero, which is what you have here, means branch if boolean is false. And that is ceq against 0 followed by brtrue.

That said, it's worth pointing out that when compiling for debug performance is not an issue. The compiler wants to write code to make it possible for the debugger to inspect variables. If you are interested in performance you have to look at the IL from a release build. When you do that you'll see quite different code.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Doesn't seem to answer the question. – Peter Ritchie Mar 10 '15 at 13:23
  • @Peter you understand that testing a bool for zero is the same as testing if it is false? – David Heffernan Mar 10 '15 at 13:25
  • The *temp* is the IL_0016: ceq, IL_0018: stloc.3, IL_0019: ldloc.3, so location 3 – xanatos Mar 10 '15 at 13:29
  • @david Not really, there is no `cne`. If it's a comparison between `brtrue` or `brfalse`; there can't be a difference in performance. probably just a matter of having one path of code generation instead of two by normalizing the test. – Peter Ritchie Mar 10 '15 at 13:31
  • @xanatos However you slice it, branching when a boolean is false means branching if zero. With the instructions available that's ceq followed by brtrue. Optimise away the boolean and it's a different story for sure. – David Heffernan Mar 10 '15 at 13:34
  • If the boolean is true, program execution continues into the true branch which follows. Only if the boolean is false do we want to jump to the false branch. The compiler is converting a C# code into IL that executes in order from top to bottom unless told to jump to a different location. – Grax32 Mar 10 '15 at 13:47
  • @PeterRitchie What you seem to be failing to grasp is that in the absence of `cne`, a branch if boolean is false pretty much has to be `ceq` against `0` followed by `brtrue`. The reason that the release code use `brfalse` is that it is branching off a different comparison. The release code has optimised away the boolean completely and now is doing `ceq` to compare the return value of the function with `3`. And in that scenario, `brfalse` is the correct branch instruction. I think you confusion is because you have not realised that you are not comparing like with like. – David Heffernan Mar 10 '15 at 16:26