Battle of the primitives: Double vs Decimal

By DimitriC at January 24, 2010 11:40
Filed Under: Programming

Double 

This floating-point datatype stores 64-bit floating-point values. Many of the math-functions in .NET (e.g.: Sqrt() ) uses doubles for it's calculations. Numbers ranging from negative 1.79769313486232e308 to positive 1.79769313486232e308 can be stored in a double and it also has the ability to represent positive and negative infinity. Converting a double to a decimal might result in an exception (OverflowException) when the value can not be represented as a decimal.

Type Approximate Range Precision .NET Framework Type
double

±5.0 × 10−324 to ±1.7 × 10308

15-16 digits

System.Double

(source: MSDN)

Since a double holds less precision it's not a good idea to compare this to another double. Chances are that because of rounding, you might get errors. For "greater than" or "smaller than" comparisons a double does just fine.

The behavior of a double isn't always what you would expect it to be. When dividing a decimal by zero you get a DivideByZero-exception. A double does not. Instead it returns values such as "infinity":

   1: double f = 3.965478d;
   2: Console.WriteLine("divide by zero: " + (f / 0));

doubleVsDecimal2

Also, different ways to lead to a certain result, don't always lead to the same result but rather to something near it. To illustrate this I got a great example from Steve McConnel's book "Code Complete, 2nd Edition". The idea is very simple: create a variable and add 0.1 ten times in a row (which results to 1.0) then compare it with an expected result: 1.0.

   1: double sum = 0.0d;
   2: double expected = 1.0d;
   3:  
   4: for (int i = 0; i < 10; i++)
   5: {
   6:     sum += 0.1d;
   7:     Console.WriteLine(sum);
   8: }
   9:  
  10: if (sum == expected)
  11: {
  12:     Console.WriteLine("sum is equal to expected");                
  13: }
  14: else
  15: {
  16:     Console.WriteLine("sum is different from expected");
  17: }

Of course, this gives "Sum is different from expected". To see what values are used for the comparisson I set a breakpoint on line 10 and this is what you see:

doubleVsDecimal3

To solve this, you can set a range of accuracy that is acceptable and then use a boolean to evaluate whether the values are close enough. And of course, since you know that working with doubles can result in rounding errors, this can be anticipated.

Decimal

The decimal is a 128-bit data type which, due to it's relatively large size, has great precision. This precision makes a decimal perfect for calculations where this degree of precision is required (science, finance). It can represent decimal numbers ranging from positive 79,228,162,514,264,337,593,543,950,335 to negative 79,228,162,514,264,337,593,543,950,335. Converting a decimal to another data type will result in a loss of information. You might notice rounding errors or even exceptions when the result of the conversion is not representable in the requested data type. Converting a decimal to a double might also result in rounding errors, but you will not lose any information as to the magnitude of the value.

 

Type Approximate Range Precision .NET Framework Type
decimal

±1.0 × 10−28 to ±7.9 × 1028

28-29 significant digits

System.Decimal

(source: MSDN)

The decimal is build up out of four integers (4 * 32 bit = 128 bit). Thes first three represent the mantissa and the fourth integer holds the information concerning the sign and exponent of the decimal. You can get these values as an array of integers (int) by using the GetBits(decimal) command:

   1: decimal[] decimalArray = {5M, decimal.MaxValue, decimal.MinValue, 0.598M, 3.14159265M, 10000000000M};
   2: int[] bitvalues;
   3:  
   4: foreach (decimal dc in decimalArray)
   5: {
   6:     Console.WriteLine("Showing results for decimal: " + dc.ToString() );
   7:     bitvalues = decimal.GetBits(dc);
   8:  
   9:     for (int i = 0; i < 4; i++)
  10:     {
  11:         Console.WriteLine("Bit " + (i+1) + ": " + bitvalues[i]);
  12:     }
  13: }

This code gives the following output:

doubleVsDecimal1

In contrast to C# the CLR doesn't see a decimal as a primitive type. The .NET Framework SDK documentation tells us  that it has public static methods called Add, Subtract, Multiply, Divide,... And, in addition, there are operator overload methods for +,-,*,/,... When compiling, the compiler generates the code to call the members to perform the actual operation since there are no IL instructions available for manipulating values of type Decimal.

 

Double vs Decimal

Of course, both these types have their pro's and con's. When comparing performance you might expect the decimal being a lot slower than the double since it's actually not a primitive and so the compiler has to do a lot of other stuff before doing the actual calculation. And this assumption is correct, the following example is only to show how much slower the decimal is compared to the double. The conclusion comes down to this: it's performance versus correctness. The double is way faster but has the rounding problem. The decimal is slower but has more precission. For financial institutions or any form of monetary transaction for that matter, it's obvious to choose a decimal over a double.

I've created two loops: one with a double, one with a decimal. These loops will count to one million and add 1 to the double and the decimal (which have been instantiated to 0). This two loops are timed and when the loop is done, the results are displayed (the decimal itself, the elapsed ticks and the elapsed milliseconds).

   1: static void Main(string[] args)
   2:         {
   3:             Stopwatch stopwatch = new Stopwatch();
   4:             double doubleResult = 0.0d;
   5:             decimal decimalResult = 0.0M;
   6:  
   7:             stopwatch.Start();
   8:             for (int i = 0; i < 1000000; i++)
   9:             {
  10:                 doubleResult += 1;
  11:             }
  12:             stopwatch.Stop();
  13:             Console.WriteLine("The doubleResult is: " + doubleResult);
  14:             Console.WriteLine("Elapsed Ticks: " + stopwatch.ElapsedTicks);
  15:             Console.WriteLine("Elapsed Miliseconds: " + stopwatch.ElapsedMilliseconds);
  16:  
  17:             stopwatch.Reset();
  18:  
  19:             stopwatch.Start();
  20:             for (int i = 0; i < 1000000; i++)
  21:             {
  22:                 decimalResult += 1;
  23:             }
  24:             stopwatch.Stop();
  25:             Console.WriteLine("The decimalResult is: " + decimalResult);
  26:             Console.WriteLine("Elapsed Ticks: " + stopwatch.ElapsedTicks);
  27:             Console.WriteLine("Elapsed Miliseconds: " + stopwatch.ElapsedMilliseconds);
  28:  
  29:             Console.ReadLine();
  30:         }

The result:

doubleVsDecimal4

Now I execute the same program, but instead of adding 1, I add 0.1. Here we see another example of the possible rounding errors you get when working with doubles. The result:

doubleVsDecimal5

Comments are closed