As a follow up of the question I posted yesterday, Mathematics and generics I decided to go ahead and put together a simple RealNumber
class wrapping decimal
to enable generic mathematics and do some simple tests to see performance compared (please do not comment on why, what for or how to implement a wrapper for decimal, that is not the aim of this question).
I decided to compare the following 2 implementations:
RealNumberStruct
: interface optionRealNumberClass
: abstract base class option
Code for RealNumberStruct
is as follows:
internal interface IArithmetic : IEquatable<IArithmetic>
{
IArithmetic Add(IArithmetic right);
IArithmetic Subtract(IArithmetic right);
IArithmetic Multiply(IArithmetic right);
IArithmetic Divide(IArithmetic right);
IArithmetic Negate();
}
public struct RealNumberStruct : IArithmetic
{
private readonly decimal value;
private RealNumberStruct(decimal d)
{
this.value = d;
}
public static implicit operator decimal(RealNumberStruct value)
{
return value.value;
}
public static implicit operator RealNumberStruct(decimal value)
{
return new RealNumberStruct(value);
}
public static RealNumberStruct operator +(RealNumberStruct left, RealNumberStruct right)
{
return new RealNumberStruct(left.value + right.value);
}
public static RealNumberStruct operator -(RealNumberStruct value)
{
return new RealNumberStruct(-value.value);
}
public static RealNumberStruct operator -(RealNumberStruct left, RealNumberStruct right)
{
return new RealNumberStruct(left.value - right.value);
}
public static RealNumberStruct operator *(RealNumberStruct left, RealNumberStruct right)
{
return new RealNumberStruct(left.value * right.value);
}
public static RealNumberStruct operator /(RealNumberStruct left, RealNumberStruct right)
{
return new RealNumberStruct(left.value / right.value);
}
IArithmetic IArithmetic.Add(IArithmetic right)
{
if (!(right is RealNumberStruct))
throw new ArgumentException();
return this + (RealNumberStruct)right;
}
IArithmetic IArithmetic.Subtract(IArithmetic right)
{
if (!(right is RealNumberStruct))
throw new ArgumentException();
return this - (RealNumberStruct)right;
}
IArithmetic IArithmetic.Multiply(IArithmetic right)
{
if (!(right is RealNumberStruct))
throw new ArgumentException();
return this * (RealNumberStruct)right;
}
IArithmetic IArithmetic.Divide(IArithmetic right)
{
if (!(right is RealNumberStruct))
throw new ArgumentException();
return this / (RealNumberStruct)right;
}
IArithmetic IArithmetic.Negate()
{
return -this;
}
bool IEquatable<IArithmetic>.Equals(IArithmetic other)
{
throw new NotImplementedException();
}
}
Code for RealNumberClass
is as follows:
public abstract class Arithmetic: IEquatable<Arithmetic>
{
protected abstract Arithmetic _Add(Arithmetic right);
protected abstract Arithmetic _Subtract(Arithmetic right);
protected abstract Arithmetic _Multiply(Arithmetic right);
protected abstract Arithmetic _Divide(Arithmetic right);
protected abstract Arithmetic _Negate();
internal Arithmetic Add(Arithmetic right) { return _Add(right); }
internal Arithmetic Subtract(Arithmetic right) { return _Subtract(right); }
internal Arithmetic Multiply(Arithmetic right) { return _Multiply(right); }
internal Arithmetic Divide(Arithmetic right) { return _Divide(right); }
internal Arithmetic Negate() { return _Negate(); }
public abstract bool Equals(Arithmetic other);
}
public class RealNumberClass : Arithmetic
{
private readonly decimal value;
private RealNumberClass(decimal d)
{
this.value = d;
}
public static implicit operator decimal(RealNumberClass value)
{
return value.value;
}
public static implicit operator RealNumberClass(decimal value)
{
return new RealNumberClass(value);
}
public static RealNumberClass operator +(RealNumberClass left, RealNumberClass right)
{
return new RealNumberClass(left.value + right.value);
}
public static RealNumberClass operator -(RealNumberClass value)
{
return new RealNumberClass(-value.value);
}
public static RealNumberClass operator -(RealNumberClass left, RealNumberClass right)
{
return new RealNumberClass(left.value - right.value);
}
public static RealNumberClass operator *(RealNumberClass left, RealNumberClass right)
{
return new RealNumberClass(left.value * right.value);
}
public static RealNumberClass operator /(RealNumberClass left, RealNumberClass right)
{
return new RealNumberClass(left.value / right.value);
}
protected override Arithmetic _Add(Arithmetic right)
{
if (!(right is RealNumberClass))
throw new ArgumentException();
return this + (RealNumberClass)right;
}
protected override Arithmetic _Subtract(Arithmetic right)
{
if (!(right is RealNumberClass))
throw new ArgumentException();
return this - (RealNumberClass)right;
}
protected override Arithmetic _Multiply(Arithmetic right)
{
if (!(right is RealNumberClass))
throw new ArgumentException();
return this * (RealNumberClass)right;
}
protected override Arithmetic _Divide(Arithmetic right)
{
if (!(right is RealNumberClass))
throw new ArgumentException();
return this / (RealNumberClass)right;
}
protected override Arithmetic _Negate()
{
return -this;
}
public override bool Equals(Arithmetic other)
{
throw new NotImplementedException();
}
}
Now, If I go ahead and test this code with the following code:
static void TestPerformance(int outerCount)
{
int count = 0;
do
{
var stopWatch = new Stopwatch();
int repetitions = 100000;
testRealNumberStruct(1);
testRealNumberClass(1);
testDecimal(1);
double structAverage = 0, classAverage = 0, decimalAverage = 0;
for (int i = 0; i < outerCount; i++)
{
Console.WriteLine();
stopWatch.Start();
testRealNumberStruct(repetitions);
stopWatch.Stop();
structAverage += stopWatch.ElapsedMilliseconds;
Console.WriteLine("RealNumber struct test: {0} ms", stopWatch.ElapsedMilliseconds);
stopWatch = new Stopwatch();
stopWatch.Start();
testRealNumberClass(repetitions);
stopWatch.Stop();
classAverage += stopWatch.ElapsedMilliseconds;
Console.WriteLine("RealNumber class test: {0} ms", stopWatch.ElapsedMilliseconds);
stopWatch.Reset();
stopWatch = new Stopwatch();
stopWatch.Start();
testDecimal(repetitions);
stopWatch.Stop();
decimalAverage += stopWatch.ElapsedMilliseconds;
Console.WriteLine("Decimal test: {0} ms", stopWatch.ElapsedMilliseconds);
Console.WriteLine();
}
Console.WriteLine();
Console.WriteLine("Test #{0} results----------------------------------", ++count);
Console.WriteLine("RealNumber struct average: {0:F0} ms", structAverage / outerCount);
Console.WriteLine("RealNumber class average: {0:F0} ms", classAverage / outerCount);
Console.WriteLine("Decimal average: {0:F0} ms", decimalAverage / outerCount);
} while (Console.ReadKey().Key != ConsoleKey.Q);
}
private static void testRealNumberStruct(int repetitions)
{
for (int i = 0; i < repetitions; ++i)
{
IArithmetic d1 = (RealNumberStruct)1.25m;
IArithmetic d2 = (RealNumberStruct)(-0.25m);
var d = d1.Multiply(d2);
d = d.Add(d1);
d = d2.Divide(d);
d = d1.Subtract(d);
}
}
private static void testRealNumberClass(int repetitions)
{
for (int i = 0; i < repetitions; ++i)
{
Arithmetic d1 = (RealNumberClass)1.25m;
Arithmetic d2 = (RealNumberClass)(-0.25m);
var d = d1.Multiply(d2);
d = d.Add(d1);
d = d2.Divide(d);
d = d1.Subtract(d);
}
}
private static void testDecimal(int repetitions)
{
for (int i = 0; i < repetitions; ++i)
{
var d1 = 1.25m;
var d2 = -0.25m;
var d = d1 * d2;
d = d + d1;
d = d2 / d;
d = d1 - d;
}
}
I'm consistently getting what I consider a weird behavior. Output of the test (outerCount = 3
) is the following:
RealNumber struct test: 40 ms
RealNumber class test: 35 ms
Decimal test: 29 ms
RealNumber struct test: 64 ms
RealNumber class test: 32 ms
Decimal test: 27 ms
RealNumber struct test: 62 ms
RealNumber class test: 33 ms
Decimal test: 27 ms
Test #1 results-----------------------------------
RealNumber struct average: 55 ms
RealNumber class average: 33 ms
Decimal average: 28 ms
Note that in the first run, the performance of RealNumberStruct
and RealNumberClass
are similar (40 ms vs 35 ms), but in runs 2 and 3 RealNumberStruct
drops away (62 and 52 ms) while performance of RealNumberClass
and decimal
remain constant. This seems to be a consistent behavior no matter how many runs I do; the first run is always considerably faster. Can someone tell me why this is happening? Is it the GC somehow getting in the way?
Test is run in release build outside the debugger. Can anyone else reproduce this behavior?
EDIT: Fixed some typos in code.