C# Decimal datatype performance

C#PerformanceDecimal

C# Problem Overview


I'm writing a financial application in C# where performance (i.e. speed) is critical. Because it's a financial app I have to use the Decimal datatype intensively.

I've optimized the code as much as I could with the help of a profiler. Before using Decimal, everything was done with the Double datatype and the speed was several times faster. However, Double is not an option because of its binary nature, causing a lot of precision errors over the course of multiple operations.

Is there any decimal library that I can interface with C# that could give me a performance improvement over the native Decimal datatype in .NET?

Based on the answers I already got, I noticed I was not clear enough, so here are some additional details:

  • The app has to be as fast as it can possibly go (i.e. as fast as it was when using Double instead of Decimal would be a dream). Double was about 15x faster than Decimal, as the operations are hardware based.
  • The hardware is already top-notch (I'm running on a Dual Xenon Quad-Core) and the application uses threads, so CPU utilization is always 100% on the machine. Additionally, the app is running in 64bit mode, which gives it a mensurable performance advantage over 32bit.
  • I've optimized past the point of sanity (more than one month and a half optimizing; believe it or not, it now takes approx. 1/5000 of what it took to do the same calculations I used as a reference initially); this optimization involved everything: string processing, I/O, database access and indexes, memory, loops, changing the way some things were made, and even using "switch" over "if" everywhere it made a difference. The profiler is now clearly showing that the remaining performance culprit is on the Decimal datatype operators. Nothing else is adding up a considerable amount of time.
  • You have to believe me here: I've gone as far as I could possibly go in the realm of C#.NET to optimize the application, and I'm really amazed at its current performance. I'm now looking for a good idea in order to improve Decimal performance to something close to Double. I know it's only a dream, but just wanted to check I thought of everything possible. :)

Thanks!

C# Solutions


Solution 1 - C#

you can use the long datatype. Sure, you won't be able to store fractions in there, but if you code your app to store pennies instead of pounds, you'll be ok. Accuracy is 100% for long datatypes, and unless you're working with vast numbers (use a 64-bit long type) you'll be ok.

If you can't mandate storing pennies, then wrap an integer in a class and use that.

Solution 2 - C#

You say it needs to be fast, but do you have concrete speed requirements? If not, you may well optimise past the point of sanity :)

As a friend sitting next to me has just suggested, can you upgrade your hardware instead? That's likely to be cheaper than rewriting code.

The most obvious option is to use integers instead of decimals - where one "unit" is something like "a thousandth of a cent" (or whatever you want - you get the idea). Whether that's feasible or not will depend on the operations you're performing on the decimal values to start with. You'll need to be very careful when handling this - it's easy to make mistakes (at least if you're like me).

Did the profiler show particular hotspots in your application that you could optimise individually? For instance, if you need to do a lot of calculations in one small area of code, you could convert from decimal to an integer format, do the calculations and then convert back. That could keep the API in terms of decimals for the bulk of the code, which may well make it easier to maintain. However, if you don't have pronounced hotspots, that may not be feasible.

+1 for profiling and telling us that speed is a definite requirement, btw :)

Solution 3 - C#

The question is well discussed but since I was digging this problem for a while I would like to share some of my results.

Problem definition: Decimals are known to be much slower than doubles but financial applications cannot tolerate any artefacts that arise when calculations are performed on doubles.

Research

My aim was to measure different approaches of storing float-pointing numbers and to make a conclusion which one should be used for our application.

If was acceptable for us to use Int64 to store floating point numbers with fixed precision. Multiplier of 10^6 was giving us both: enough digits to store fractions and stil a big range to store large amounts. Of course, you have to be careful whith this approach (multiplication and division operations might become tricky), but we were ready and wanted to measure this approach as well. One thing you have to keep in mind except for possible calculation errors and overflows, is that usually you cannot expose those long numbers to public API. So all internal calculations could be performed with longs but before sending the numbers to the user they should be converted to something more friendly.

I've implemented a simple prototype class that wraps a long value to a decimal-like structure (called it Money) and added it to the measurments.

public struct Money : IComparable
{
	private readonly long _value;

	public const long Multiplier = 1000000;
	private const decimal ReverseMultiplier = 0.000001m;

	public Money(long value)
	{
		_value = value;
	}

	public static explicit operator Money(decimal d)
	{
		return new Money(Decimal.ToInt64(d * Multiplier));
	}

	public static implicit operator decimal (Money m)
	{
		return m._value * ReverseMultiplier;
	}

	public static explicit operator Money(double d)
	{
		return new Money(Convert.ToInt64(d * Multiplier));
	}

	public static explicit operator double (Money m)
	{
		return Convert.ToDouble(m._value * ReverseMultiplier);
	}

	public static bool operator ==(Money m1, Money m2)
	{
		return m1._value == m2._value;
	}

	public static bool operator !=(Money m1, Money m2)
	{
		return m1._value != m2._value;
	}

	public static Money operator +(Money d1, Money d2)
	{
		return new Money(d1._value + d2._value);
	}

	public static Money operator -(Money d1, Money d2)
	{
		return new Money(d1._value - d2._value);
	}

	public static Money operator *(Money d1, Money d2)
	{
		return new Money(d1._value * d2._value / Multiplier);
	}

	public static Money operator /(Money d1, Money d2)
	{
		return new Money(d1._value / d2._value * Multiplier);
	}

	public static bool operator <(Money d1, Money d2)
	{
		return d1._value < d2._value;
	}

	public static bool operator <=(Money d1, Money d2)
	{
		return d1._value <= d2._value;
	}

	public static bool operator >(Money d1, Money d2)
	{
		return d1._value > d2._value;
	}

	public static bool operator >=(Money d1, Money d2)
	{
		return d1._value >= d2._value;
	}

	public override bool Equals(object o)
	{
		if (!(o is Money))
			return false;

		return this == (Money)o;
	}

	public override int GetHashCode()
	{
		return _value.GetHashCode();
	}

	public int CompareTo(object obj)
	{
		if (obj == null)
			return 1;

		if (!(obj is Money))
			throw new ArgumentException("Cannot compare money.");

		Money other = (Money)obj;
		return _value.CompareTo(other._value);
	}

	public override string ToString()
	{
		return ((decimal) this).ToString(CultureInfo.InvariantCulture);
	}
}

Experiment

I measured following operations: addition, subtraction, multiplication, division, equality comparison and relative (greater/less) comparison. I was measuring operations on the following types: double, long, decimal and Money. Each operation was performed 1.000.000 times. All numbers were pre-allocated in arrays, so calling custom code in constructors of decimal and Money should not affect the results.

Added moneys in 5.445 ms
Added decimals in 26.23 ms
Added doubles in 2.3925 ms
Added longs in 1.6494 ms

Subtracted moneys in 5.6425 ms
Subtracted decimals in 31.5431 ms
Subtracted doubles in 1.7022 ms
Subtracted longs in 1.7008 ms

Multiplied moneys in 20.4474 ms
Multiplied decimals in 24.9457 ms
Multiplied doubles in 1.6997 ms
Multiplied longs in 1.699 ms

Divided moneys in 15.2841 ms
Divided decimals in 229.7391 ms
Divided doubles in 7.2264 ms
Divided longs in 8.6903 ms

Equility compared moneys in 5.3652 ms
Equility compared decimals in 29.003 ms
Equility compared doubles in 1.727 ms
Equility compared longs in 1.7547 ms

Relationally compared moneys in 9.0285 ms
Relationally compared decimals in 29.2716 ms
Relationally compared doubles in 1.7186 ms
Relationally compared longs in 1.7321 ms

Conclusions

  1. Addition, subtraction, multiplication, comparison operations on decimal are ~15 times slower than operations on long or double; division is ~30 times slower.
  2. Performance of Decimal-like wrapper is better than performance of Decimal but still significantly worse than performance of double and long due to lack of support from CLR.
  3. Performing calculations on Decimal in absolute numbers is quite fast: 40.000.000 operations per second.

Advice

  1. Unless you have a very heavy calculation case, use decimals. In relative numbers they are slower than longs and doubles, but absolute numbers look good.
  2. There is not much point in re-implementing Decimal with your own structure due to abcense of support from CLR. You might make it faster than Decimal but it will never be as fast as double.
  3. If performance of Decimal is not enough for your application, than you might want consider switching your calculations to long with fixed precision. Before returning the result to the client it should be converted to Decimal.

Solution 4 - C#

The problem is basically that double/float are supported in hardware, while Decimal and the like are not. I.e. you have to choose between speed + limited precision and greater precision + poorer performance.

Solution 5 - C#

I don't think that SSE2 instructions could easy work with .NET Decimal values. .NET Decimal data type is 128bit decimal floating point type http://en.wikipedia.org/wiki/Decimal128_floating-point_format, SSE2 instructions work with 128bit integer types.

Solution 6 - C#

4 years after my previous answer I would like to add another one based on the experience we had over the years on working with high-performance computations with floating-point numbers.

There are two major problems with Decimal data type on high-performance computations:

  1. CLR treats this type as a regular structure (no special support as for other built-in types)
  2. It is 128 bit

While you cannot do much about the first issue, second looks even more important. Memory operations and processors are extremely efficient when operating with 64-bit numbers. 128-bit operations are much heavier. Thus .NET implementation of Decimal is by design significantly slower that the operation on Double even for read/write operations.

If your application needs both the accuracy of floating-point computations and performance of such operations then neither Double or Decimal are suitable for the task. The solution that we have adopted in my company (Fintech domain) is to use a wrapper on top of IntelĀ® Decimal Floating-Point Math Library. It implements the IEEE 754-2008 Decimal Floating-Point Arithmetic specification providing 64-bit floating-point decimals.

Remarks. Decimals should only be used for storing the floating-point numbers and simple arithmetic operations on them. All heavy mathematics like calculating indicators for technical analysis should be performed on Double values.

UPD 2020: We have open-sourced the library for decimals DFP. It is bi-lingual (C# and java). There are some peculiarities for java keeping in mind that you cannot have custom non-allocating types (structures) in java. But this is out of scope in terms of this discussion. Feel free to use.

Solution 7 - C#

Old question, still very valid though.

Here are some numbers to support the idea of using Long.

Time taken to perform 100'000'000 additions

Long     231 mS
Double   286 mS
Decimal 2010 mS

in a nutshell, decimal is ~10 times slower that Long or Double.

Code:

Sub Main()
    Const TESTS = 100000000
    Dim sw As Stopwatch

    Dim l As Long = 0
    Dim a As Long = 123456
    sw = Stopwatch.StartNew()
    For x As Integer = 1 To TESTS
        l += a
    Next
    Console.WriteLine(String.Format("Long    {0} mS", sw.ElapsedMilliseconds))

    Dim d As Double = 0
    Dim b As Double = 123456
    sw = Stopwatch.StartNew()
    For x As Integer = 1 To TESTS
        d += b
    Next
    Console.WriteLine(String.Format("Double  {0} mS", sw.ElapsedMilliseconds))

    Dim m As Decimal = 0
    Dim c As Decimal = 123456
    sw = Stopwatch.StartNew()
    For x As Integer = 1 To TESTS
        m += c
    Next
    Console.WriteLine(String.Format("Decimal {0} mS", sw.ElapsedMilliseconds))

    Console.WriteLine("Press a key")
    Console.ReadKey()
End Sub

Solution 8 - C#

I cannot give a comment or vote down yet since I just started on stack overflow. My comment on alexsmart (posted 23 Dec 2008 12:31) is that the expression Round(n/precision, precision), where n is int and precisions is long will not do what he thinks:

  1. n/precision will return an integer-division, i.e. it will already be rounded but you won't be able to use any decimals. The rounding behavior is also different from Math.Round(...).

  2. The code "return Math.Round(n/precision, precision).ToString()" does not compile due to an ambiguity between Math.Round(double, int) and Math.Round(decimal, int). You will have to cast to decimal (not double since it is a financial app) and therefore can as well go with decimal in the first place.

  3. n/precision, where precision is 4 will not truncate to four decimals but divide by 4. E.g., Math.Round( (decimal) (1234567/4), 4) returns 308641. (1234567/4 = 308641.75), while what you probably wanted to to is get 1235000 (rounded to a precision of 4 digits up from the trailing 567). Note that Math.Round allows to round to a fixed point, not a fixed precision.

Update: I can add comments now but there is not enough space to put this one into the comment area.

Solution 9 - C#

What about MMX/SSE/SSE2?

i think it will help... so... decimal is 128bit datatype and SSE2 is 128bit too... and it can add, sub, div, mul decimal in 1 CPU tick...

you can write DLL for SSE2 using VC++ and then use that DLL in your application

e.g //you can do something like this

VC++

#include <emmintrin.h>
#include <tmmintrin.h>

extern "C" DllExport __int32* sse2_add(__int32* arr1, __int32* arr2);

extern "C" DllExport __int32* sse2_add(__int32* arr1, __int32* arr2)
{
	__m128i mi1 = _mm_setr_epi32(arr1[0], arr1[1], arr1[2], arr1[3]);
	__m128i mi2 = _mm_setr_epi32(arr2[0], arr2[1], arr2[2], arr2[3]);

	__m128i mi3 = _mm_add_epi32(mi1, mi2);
	__int32 rarr[4] = { mi3.m128i_i32[0], mi3.m128i_i32[1], mi3.m128i_i32[2], mi3.m128i_i32[3] };
	return rarr;
}

C#

[DllImport("sse2.dll")]
private unsafe static extern int[] sse2_add(int[] arr1, int[] arr2);

public unsafe static decimal addDec(decimal d1, decimal d2)
{
    int[] arr1 = decimal.GetBits(d1);
    int[] arr2 = decimal.GetBits(d2);
    
    int[] resultArr = sse2_add(arr1, arr2);

    return new decimal(resultArr);
}

Solution 10 - C#

store "pennies" using double. apart from parsing input and printing outputs, you have the same speed you measured. you overcome the limit of 64 bit integer. you have a division not truncating. note : is up to you how to use the double result after divisions. this seems to me the simplest approach to your requirements.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiontempwView Question on Stackoverflow
Solution 1 - C#gbjbaanbView Answer on Stackoverflow
Solution 2 - C#Jon SkeetView Answer on Stackoverflow
Solution 3 - C#user1921819View Answer on Stackoverflow
Solution 4 - C#Brian RasmussenView Answer on Stackoverflow
Solution 5 - C#Sergey ShandarView Answer on Stackoverflow
Solution 6 - C#user1921819View Answer on Stackoverflow
Solution 7 - C#smirkingmanView Answer on Stackoverflow
Solution 8 - C#ILoveFortranView Answer on Stackoverflow
Solution 9 - C#RunknownView Answer on Stackoverflow
Solution 10 - C#MassimoView Answer on Stackoverflow