Numbers in .NET
Working in Excel, I encountered all sorts of issues with floating point numbers. Excel had to deal with NaNs (of which there are many), +/- infinity, negative zeros (1/NegativeInfinity), and denormalized numbers (numbers smaller than 1/double.MaxValue but larger than zero). In addition, there were also a number of rounding issues, with which we solved with Visual Rounding (converting a number to its visual representation in base ten) and chopping the result of a difference to zero if the result is 16 decimal places (precision) away from the operands.
It's funny how a lot of familiar assumptions break down because using numbers in computers. .NET introduces a number of new issues with numeric types.
- +/- Infinity and NaNs are more likely to occur in .NET than in the past. Double & Single do not trigger Division By Zero exceptions as they do in other languages such as C++ (and probably even C++/CLI).
- Int32 (int) can safely be converted to higher order types (Decimal, Double, and Int64) except Single as it can be represented completely and accurately in any one of those representations.
- Conversions to Double/Single never cause exceptions, because of the ability to represent the full range of numbers, possibly by losing precision or by collapsing a large value to infinity or a small value to zero. Notably, a double to single conversion always succeeds.
- Both Int64 and Decimal have higher precision, 64 and 96 bits, respectively, than the floating-point cousins. Thus, neither can be accurately represented by a Double or Single. So, for example, (Int64) (Double) Int64.MaxValue < Int64.MaxValue. Despite the higher precision, both types have lower range and can produce overflow exceptions when converting from a floating-point value.
- Decimal values have the fewest surprises, since it uses a base 10 scale and has almost twice the precision of a double. However, one quirk is that the same value can have two different string representations: For example, 1.00m and 1m print out differently, yet are equals for all purposes.
|Single||4||23+1 bits (7 digits)||10 ^ 38|
|Double||8||52+1 bits (15 digits)||10 ^ 308|
|Int32||4||32 bits (9 digits)||10 ^ 9|
|Int64||8||64 bits (19 digits)||10 ^ 19|
|Decimal||16||96 bits (28 digits)||10 ^ 28|
Floating-point numbers (Single & Double) are as fast as integral types (Int32 & Int64) and even slightly faster in certain operations such as multiplication and division, so speed is never really a factor in the decision to use one over the other except when using a floating-number requires it to be cast into an integral type first. However, the Decimal type, which is not supported in hardware, is almost 3 orders of magnitude slower than the other types.
There's no single universal numeric type; you have to trade off range and performance against precision and accuracy. I never really understood why the Decimal type couldn't have allowed a scale that could encompass the full range of Double values.
Anyway, in my quiz, I came up with various assumptions we have that are ordinarily true about numbers in the real world, but prove to be false within the realm of computers. Unfortunately, two of my questions are ambiguous and another one is incorrectly written. It wasn't suppose to be a hard test. In practice, these assumptions are almost always valid, because the edge cases in which they fail are rare--basically because the magnitudes in edge case are high, approximating infinity.
If x and y are ints, ....
- If x < 0, then -x > 0. False, because the negation of int.MinValue is itself. This is due to the assymetry of having more negative values than positive values. Note that such an action would throw an overflow exception if checked arithmetic is enabled.
- If x = -x, then x - 0. False, for the same reasons as above.
- If x - y > 0, then x > y. False, because of overflow such as in the case int.MinValue-int.MaxValue = 1. This can only fail if any of operands are greater than or equal to 2^30 in magnitude.
- If x and y are positive, then x + y > x. False, because of overflow as in the case of int.MaxValue + int.MaxValue == -2.
- If x and y are positive, then (double)x * (double)y = (long)x * (long)y. False. The question is ambiguous because of casting issues. The product of two positive ints fits into a long but not necessarily in a double due to loss of precision.
- If x-y>0 and y-z>0, then x-z>0. False, because of overflow such as with x=int.MaxValue-1, y=-1, and z=int.MinValue. I was testing out the Law of Transitivity for inequality and this was the best violation I could come up with.
Most problems with ints can be checked by turning on overflow checking. This is why I leave checked mode turned on in debug mode.
If x and y are doubles, ...
- x = x. False, NaNs behave like database nulls, returning false for all comparison operators except inequality.
- If x>y is false, then x<=y is true. False, due to NaNs.
- If x > 0, then x - x = 0. False, because difference of two Infinite value of the same sign is a NaN. The ratio of two Infinite values is also NaN.
- If x and y are positive integers, then x + y > x. False, due to precision loss. If x/y is greater than 1o^16, than y essentially becomes zero and x + y = x.
- If x and y are positive integers, then the statement "x + y = x for all y" is false for all x. True. If x were PositiveInfinity, the conclusion would be false, but the premise precludes that possibility. If x were MaxValue, the statement "x + y = x" is true for all 0< y < approx. 10e+292, which is all the numbers that people use.
- If x <= 0 is false, then x > 0. False, due to NaNs. I was initially thinking about negative zeros here, but negative zeros are not distinguishable from zeros except in the case of division by zero.
- If x and y are longs, then (double)(x + y) = (long)(x + y). False. This is a somewhat ambiguous question, but I was trying to emphasize potential loss of precision when converting a long to a double. For example, (long) (double) long.MaxValue doesn't equal long.MaxValue.
- if x.Equals(y), then x = y. False, NaNs will Equals themselves, which is necessary for collections operations to work.
- If x.Equals(-x), then x = 0. False, the Negation of a NaN is itself.
- if x.ComparesTo(y) < 0, then x < y. False, NaNs evaluate to -1 against all other value.
In my second quiz,
- If x and y are double and x = y, then 1/x = 1/y. If x is zero and y is negative zero, the 1/x is positive infinity and 1/y is negative infinity. Negative zero is obtained by operation which leads to negative underflow such as 1e-200/1e200 or 1/NegativeInfinity and is normally indistinguable from positive zero.
- If x is an int, then x >> 1 = x / 2. False, this statement fails for negative odd numbers. The arithmetic shift right operator performs a floor division by 2, where as the division operator truncates toward zero.
- If x and y are decimal and x = y, then x.ToString() == y.ToString(). False. 1.00m and 1m are equal in value but have different internal and string representations.
- 1 / ( 1/ x ) = x. False. The reciprocal of double.Epsilon and other denormalized numbers is Infinity.
- If x is a long, (long) (double) x = x. False, If x is long.MaxValue, conversion to a double loses precision.
- If x is a double and an integer and y = x, then x++ causes x > y. False, adding one to 10^17 or greater doesn't change the value.
The following mathematical laws may not work with doubles due to rounding errors.
- Associative Law: a + (b + c) = (a + b) + c.
Fails when int.MaxValue + (int.MaxValue - int.MaxValue).
- Commutative Law: a + b = b + a.
- Distributive Law: a( b + c ) = ab + bc.
Fails when a is +-Infinity and b and c have opposite signs.