Exploiting multiplication by zero optimization (and failing), yet learning about equals to zero shortcut (Roslyn)
Basically by accident I found out that multiplication by zero is optimized in Roslyn compiler. That got me thinking… Can I abuse it and make it fail?
Let me start with a small introduction of the optimization. If you multiply any value by zero and compare it to zero, the compiler will optimize the compare operation away. Because math tells us it to be always true. Here’s an example.
static int Foo()
{
return 10;
}
static int Bar()
{
return 20;
}
static void Main(string[] args)
{
if (Foo() * 0 == 0)
{
Bar();
}
}
This, with optimizations turned on, results in the following IL.
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 13 (0xd)
.maxstack 8
IL_0000: call int32 ConsoleApp1.Program::Foo()
IL_0005: pop
IL_0006: call int32 ConsoleApp1.Program::Bar()
IL_000b: pop
IL_000c: ret
} // end of method Program::Main
As you can see, the if
condition is clearly not there. Then… What if I derive from Int32
and override the *
operator. That of course failed, because I obviously can’t derive from Int32
. So I at least created my own type and overrode the *
operator.
class MyInt
{
int I;
public MyInt(int i)
{
I = i;
}
public static MyInt operator *(MyInt i, MyInt j) => 1;
public static implicit operator MyInt(int i) => new MyInt(i);
public static implicit operator int(MyInt i) => i.I;
}
I also overrode the conversion operators from and to int
for the convenience sake. The *
operator now returns 1
which would make the optimization invalid would it happen.
As you probably expect, it does not happen. It would also produce wrong code would the *
had some side-effects for example. Roslyn developers are smart enough to have this covered.
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 30 (0x1e)
.maxstack 8
IL_0000: call class ConsoleApp1.MyInt ConsoleApp1.Program::Foo()
IL_0005: ldc.i4.0
IL_0006: call class ConsoleApp1.MyInt ConsoleApp1.MyInt::op_Implicit(int32)
IL_000b: call class ConsoleApp1.MyInt ConsoleApp1.MyInt::op_Multiply(class ConsoleApp1.MyInt,
class ConsoleApp1.MyInt)
IL_0010: call int32 ConsoleApp1.MyInt::op_Implicit(class ConsoleApp1.MyInt)
IL_0015: brtrue.s IL_001d
IL_0017: call class ConsoleApp1.MyInt ConsoleApp1.Program::Bar()
IL_001c: pop
IL_001d: ret
} // end of method Program::Main
But, before you start feeling depressed and sad, look at the IL. Can you see the comparison? No? The == 0
is really not there, but it’s converted to brtrue
(short (s
) version in this case) operation, which jumps if the value is true
, not null or non-zero. Smart!
At least something interesting.