Search code examples
c#.netdecimalrounding-error

Accumulating errors with decimal type?


I have an application where I accumulate decimal values (both adding and subtracting.) I use the decimal type rather than double in order to avoid accumulation errors. However, I've run into a case where the behavior is not quite what I'd expect.

I have x = a + b, where a = 487.5M and b = 433.33333333333333333333333335M.

Computing the addition, I get x = 920.8333333333333333333333334M.

I then have y = 967.8750000000000000000000001M.

I want to assert that y - x = y - a - b. However,

y - x = 47.0416666666666666666666667

y - a - b = 47.04166666666666666666666675

I thought this kind of error was exactly what the decimal type was intended to avoid, so what's happening here?

Here is code that reproduces the issue:

    static void Main()
    {
        decimal a = 487.5M;
        decimal b = 433.33333333333333333333333335M;
        decimal x = a + b;

        decimal y = 967.8750000000000000000000001M;

        Console.WriteLine(y - x);
        Console.WriteLine(y - a - b);
        if (y - x != y - a - b)
            Console.WriteLine("x - y != y - a - b");
        Console.ReadKey();
    }

There was some discussion in comments as to why these high precisions are necessary, so I thought I'd address in summary here. For display purposes, I certainly round the results of these operations, but I use decimal for all internal representations. Some of the computations take fractions along the way, which results in numbers that are beyond the precision of the decimal type.

I take care, however, to try and keep everything stable for accumulation. So, for instance, if I split up a quantity into three thirds, I take x/3, x/3 and then (x - x/3 - x/3). This is a system that is accounting for physical quantities that are often divided up like this, so I don't want to introduce biases by rounding too soon. For instance, if I rounded the above for x=1 to three decimals, I would wind up with 0.333, 0.333, 0.334 as the three portions of the operation.

There are real physical limitations to the precision of what the system can do, but the logical accounting of what it's trying to do should ideally stay as precise as it can. The main critical requirement is that the sum total quantity of the system should not change as a result of these various operations. In the above case, I'm finding that decimal can violate this assumption, so I want to understand better why this is happening and how I can fix it.


Solution

  • The C# type Decimal is not like the decimal types used in COBOL, which actually store the numbers one decimal digit per nibble, and uses mathematical methods similar to doing decimal math by hand. Rather, it is a floating point type that simply assumes quantities will not get so large, so it uses fewer bits for exponents, and uses the remaining the bits of 128 rather than 64 for double to allow for greatly increased accuracy.

    But being a floating point representation, even very simply fractional values are not represented exactly: 0.1, for example, requires a binary repeating fraction and may not be stored as an exact value. (It is not, for a double; Decimal may handle that particular value differently, but this is true in general.)

    Therefore comparisons still need to be made using typical floating point math procedures, in which values are compared, added, subtracted, etc., by accepting them only to a certain point. Since there are approximately 23 decimal places of accuracy, select 16 as your standard, for example, and ignore those at the end.

    For a good reference, read What Every Computer Scientist Should Know About Floating Point Precision.