Domanda

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.

È stato utile?

Soluzione

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.

Altri suggerimenti

The Decimal type is a floating-point type which has more bits of precision than any of the other types that have been built into .NET from the beginning, and whose values are all concisely representable in base-10 format. It is, however, bulky and slow, and because it is a floating-point type it is no more able to satisfy axioms typical of "precise" types (e.g. for any X and Y, (X+Y-Y)==X should either return true or throw an overflow exception). I would guess that it was made a floating-point type rather than fixed-point because of indecision regarding the number of digits that should be to the right of the decimal. In practice, it might would have been faster, and just as useful, to have a 128-bit fixed-point format, but the Decimal type is what it is.

Incidentally, languages like PL/I work well with fixed-point types because they recognize that precision is a function of a storage location rather than a value. Unfortunately, .NET does not provide any nice means via which a variable could be defined as holding a Fixed(6,3) and automatically scale and shift a Fixed(5,2) which is stored into it. Having the precision be part of the value means that storing a value into a variable will change the number of digits that variables represents to the right of the decimal place.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top