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).