tabs ↹ over ␣ ␣ ␣ spaces

by Jiří {x2} Činčura

Exploiting multiplication by zero optimization (and failing), yet learning about equals to zero shortcut (Roslyn)

1 Nov 2018 2 mins 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.

Profile Picture Jiří Činčura is .NET, C# and Firebird expert. He focuses on data and business layers, language constructs, parallelism, databases and performance. For almost two decades he contributes to open-source, i.e. FirebirdClient. He works as a senior software engineer for Microsoft. Frequent speaker and blogger at www.tabsoverspaces.com.