Math.BigMul Exposed

Today a friend and I were reflecting through System.Math (courtesy of IronPython) and we noticed the BigMul method:

Math.BigMul(Int32, Int32) : Int64

Why have a method just for multiplication? It seems to be a trivial reason to add a method to the .NET framework. After all, multiplication with casting does the same thing:

(long)a * (long)b

Being optimistic, I suggested that perhaps Microsoft’s BigMul is implementing a faster and more efficient multiplication algorithm. Maybe there is a clever way to multiply two 32 bit numbers without explicit casting to 64 bit. Naturally, I wrote a simple speed test.

static void Main(string[] args)
{
    int a = 40993;
    int b = 69872;
    long c = 0;

    DateTime start;
    TimeSpan length;

    Console.WriteLine("Inline multiplication");
    start = DateTime.Now;
    for (int i = 0; i < 1000000000; i++)
        c = (long)a * (long)b;
    length = DateTime.Now - start;
    Console.WriteLine(c);
    Console.WriteLine(length.ToString());
    Console.WriteLine();

    Console.WriteLine("Math.BigMul");
    start = DateTime.Now;
    for (int i = 0; i < 1000000000; i++)
        c = Math.BigMul(a, b);
    length = DateTime.Now - start;
    Console.WriteLine(c);
    Console.WriteLine(length.ToString());
    Console.WriteLine();

    Console.Read();
}

The results were not encouraging.

Inline multiplication
2864262896
00:00:03.9375000

Math.BigMul
2864262896
00:00:07.2031250

Then I remembered to do a Release build and run without debugging. :-D The real results:

Inline multiplication
2864262896
00:00:01.9218750

Math.BigMul
2864262896
00:00:01.9375000

After running it several times, I saw that they had basically the same performance. This begs the question: why did Microsoft even include BigMul? I cracked open Reflector and checked out the code myself. What I found surprised me.

public static long BigMul(int a, int b)
{
    return (a * b);
}

That looks like an unsafe operation to me! Disturbed, I wrote and tested my own static method using the exact same code (named BigMul2). Sure enough, my copy-cat code overflowed. Puzzled, I looked at the IL code for both methods:

.method public hidebysig static int64 BigMul(int32 a, int32 b) cil managed
{
    .maxstack 8
    L_0000: ldarg.0
    L_0001: conv.i8
    L_0002: ldarg.1
    L_0003: conv.i8
    L_0004: mul
    L_0005: ret
}

.method public hidebysig static int64 BigMul(int32 a, int32 b) cil managed
{
    .maxstack 8
    L_0000: ldarg.0
    L_0001: ldarg.1
    L_0002: mul
    L_0003: conv.i8
    L_0004: ret
}

Do you see the difference? Math.BigMul pushes a on the stack, casts a to long, pushes b on the stack, casts b to long, multiples them together and returns the result. This is a correct algorithm for multiplying two ints (but not what Reflector initially indicated). My BigMul2 method pushes a on the stack, then pushes b, multiples, then casts to long and returns the result. Apparently Reflector didn’t properly translated the code into C#. The actual C# code used by BigMul is exactly what you would expect it to be, there is nothing special going on here.

public static long BigMul(int a, int b)
{
    return (long)a * (long)b;
}

What does this all mean?

I learned a few things from this exercise.

  • Microsoft felt the need to include the trivial BigMul method although it does nothing special. It probably exists as a Best Practice device to ensure that integer multiplication is cast correctly.
  • Reflector is not reliable. It apparently drops cast operation under certain conditions. It doesn’t express the cast in C#, VB, Delphi, MC++, or Chrome – only IL. This problem is reproducible, the explicit casts in my speed-test program are also missing when viewed in Reflector.
  • I was reminded that debugging incurs a performance hit. What is especially noteworthy is the tremendous slowdown that comes from the debugger as it follows the execution stack into another method (in another assembly).