Divide by zero
Calculations involving floating point numbers do not raise exceptions, but those
involving integer data types can.
To illustrate this, see the code below, where we divide a number by zero, first
using floating point numbers and then integers. With floating point numbers the
answer is +Infinity if the numerator is positive, -Infinity if the numerator is
negative and NaN if the numerator is zero, while with integers a DivideByZero exception
is raised.
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NumericalCodeTips
{
[TestClass]
public class DivideByZeroTest
{
[TestMethod]
public void PositiveFloatingPointDivideByZero()
{
double numerator = 12.0;
double denominator = 0.0;
double result = numerator / denominator;
Assert.IsTrue(double.IsPositiveInfinity(result));
}
[TestMethod]
public void NegativeFloatingPointDivideByZero()
{
double numerator = -12.0;
double denominator = 0.0;
double result = numerator / denominator;
Assert.IsTrue(double.IsNegativeInfinity(result));
}
[TestMethod]
public void ZeroFloatingPointDivideByZero()
{
double numerator = 0.0;
double denominator = 0.0;
double result = numerator / denominator;
Assert.IsTrue(double.IsNaN(result));
}
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void PositiveIntegerDivideByZero()
{
int numerator = 12;
int denominator = 0;
int result = numerator / denominator;
}
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void NegativeIntegerDivideByZero()
{
int numerator = -12;
int denominator = 0;
int result = numerator / denominator;
}
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void ZeroIntegerDivideByZero()
{
int numerator = 0;
int denominator = 0;
int result = numerator / denominator;
}
}
}
Signed zero
Since floating point numbers in .NET conform to IEEE 754, they allow the number
zero to be positive (+0) or negative (-0). Although a test for equality on +0 and
-0 will return true, they can lead to very different results when used in subsequent
calculations, as demonstrated in the code sample below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NumericalCodeTips
{
[TestClass]
public class SignedZeroTest
{
[TestMethod]
public void SignedZero()
{
double positiveZero = +0.0;
double negativeZero = -0.0;
Assert.AreEqual(0, Math.Sign(negativeZero));
Assert.IsTrue(positiveZero == negativeZero);
Assert.IsTrue(double.IsPositiveInfinity(1.0 / positiveZero));
Assert.IsTrue(double.IsNegativeInfinity(1.0 / negativeZero));
}
}
}
Infinity
Infinity is used to signal an overflow condition, i.e. when a number is too big
(positive or negative) to be represented by the data type you are using. Infinite
values are common in numerical programming, and cannot simply be ignored. It is
also important to be aware of some subtleties when manipulating infinite values.
The code below shows how infinite values can be created and some important properties,
such as the fact that double.PositiveInfinity - double.PositiveInfinity does not
equal zero.
Note that when testing for Infinity either Double.IsPositiveInfinity(), Double.IsNegativeInfinity()
or Double.IsInfinity() should be used.
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NumericalCodeTips
{
[TestClass]
public class InfinityTest
{
[TestMethod]
public void Infinity()
{
Assert.IsTrue(double.IsPositiveInfinity(double.PositiveInfinity));
Assert.IsTrue(double.IsNegativeInfinity(double.NegativeInfinity));
Assert.IsTrue(double.IsPositiveInfinity(1.0 / 0.0));
Assert.IsTrue(double.IsNegativeInfinity(-1.0 / 0.0));
Assert.IsTrue(double.IsPositiveInfinity(double.MaxValue * 2.0));
}
[TestMethod]
public void InfiniteProperties()
{
Assert.IsFalse(0.0 == (double.PositiveInfinity - double.PositiveInfinity));
Assert.IsFalse(0.0 == (double.PositiveInfinity + double.NegativeInfinity));
Assert.IsTrue(double.IsNaN(double.PositiveInfinity / double.PositiveInfinity));
}
}
}
Rounding errors
The limited size of a floating point data type, means that numbers and arithmetic
using floating point representations can only be performed with limited accuracy.
For example 0.5 - 0.4 - 0.1 will not exactly equal 0.0, as shown in the code below.
The difference between the approximate value and the expected mathematical quantity
is call a rounding error (or round off error). This is standard behavior in programming
languages, and cannot be ignored when writing scientific code.
There are a number of important side effects of rounding errors, which software
engineers should be aware of. As described above, since 0.5-0.4-0.1 does not exactly
equal zero when using floating point arithmetic, we must take care when testing for equality. Another problem can be caused when round off errors
accumulate, often leading to disastrous consequences (e.g. the European Space Agency
Ariane rocket launched in 1996).
Rounding errors can also be caused when converting from one data type to another.
Thankfully .NET makes these issues less common due to the concept of implicit and
explicit casts.
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NumericalCodeTips
{
[TestClass]
public class RoundingErrorTest
{
[TestMethod]
public void RoundingError()
{
double x = 0.5 - 0.4 - 0.1;
Assert.IsFalse(0.0 == x);
}
}
}
Numerical equality
As explained in the section on rounding errors floating
point arithmetic has limited accuracy, meaning that 0.5 - 0.4 - 0.1 will not exactly
equal zero. Therefore care must be taken, testing for equality when numerical programming.
- If possible, remove the test for equality altogether.
- If possible change the test to use either <= or >=.
- Test that the value lies within a certain tolerance, in either absolute or relative
(percentage difference) terms.
Note however that testing whether a value lies within certain bounds, is not always
straight forward, as the choice of the tolerance, and whether to test the absolute
or relative difference will depend on how many calculations have been performed,
and the magnitude of the numbers involved. In some cases (e.g. unit tests) a simple
small absolute tolerance can be used, but in more complex situations Knuth's algorithm
for testing for equality, or a thorough numerical analysis may be required.
Not a number (NaN)
System.Double.NaN represents the result of an invalid or undefined calculation.
The code below demonstrates a number of ways to generate NaN, and how it is important
to determine if a number is NaN using the function System.Double.IsNaN() rather
than checking for equality.
When writing numerical code, it is important to check all eventualities of every
calculation. For example, every time a division is performed, is it possible that
the code might divide 0.0 by 0.0? An exception would not be raised, but the result
would be invalid.
Although out of scope of this article, it should be mentioned that there are actually
two types of NaN. The first is called a signalling NaN (SNaN or 1.#INF) denoting
that the operation was invalid, and the second is a quiet NaN (QNaN or –1.#IND),
denoting that the operation is undefined.
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NumericalCodeTips
{
[TestClass]
public class NaNTest
{
[TestMethod]
public void NaN()
{
Assert.IsTrue(double.IsNaN(double.NaN));
Assert.IsTrue(double.IsNaN(0.0 / 0.0));
Assert.IsTrue(double.IsNaN(Math.Sqrt(-1.0)));
Assert.IsTrue(double.IsNaN(double.PositiveInfinity / double.PositiveInfinity));
Assert.IsTrue(double.IsNaN(double.PositiveInfinity - double.PositiveInfinity));
}
[TestMethod]
public void NaNEquality()
{
double x = (0.0 / 0.0);
Assert.IsFalse(x == Double.NaN);
Assert.IsTrue(double.IsNaN(x));
}
}
}
Epsilon
Often numerical code uses a constant known as machine epsilon, or the square root
of machine epsilon, to compensate for the effects of rounding
errors. Machine epsilon equals the smallest number such that (1.0 + machine
epsilon) != 1.0, which equals Math.Pow(2.0, -53.0) for double precision numbers.
However, machine epsilon is not the same as the constant System.Double.Epsilon (double.Epsilon
in C#) which represents the smallest positive Double value that is greater than
zero.
These two constants have very different values, but are often confused, especially
when converting code from one language to another.
Big numbers
If you require numbers that are larger than the basic data types, note that as of
.NET 4.0 the System.Numerics namespace contains an implementation of a big integer,
appropriately called BigInteger. This allows arbitrarily large integral values to
be represented, albeit with the expected performance and storage penalties.
Checked arithmetic
By default, exceptions are raised in .NET programs if an arithmetic operation leads
to overflow or underflow using integral data types. There are two ways this can
be changed.
- Using the \checked compiler option (in Visual Studio this can be changed via the
project property page, build tab, by clicking the 'Advanced' button and changing
the 'Check for arithmetic overflow/underflow' checkbox.)
- using the checked and unchecked C# keywords in a block of code.
It is important to note that floating point arithmetic does not raise exceptions,
but instead uses the values double.PositiveInfinity, double.NegativeInfinity and
double.NaN.
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NumericalCodeTips
{
[TestClass]
public class CheckedTest
{
[ExpectedException(typeof(OverflowException))]
[TestMethod]
public void CheckedInteger()
{
checked
{
int x = int.MaxValue;
x *= 2;
}
}
[TestMethod]
public void UncheckedInteger()
{
unchecked
{
int x = int.MaxValue;
x *= 2;
}
}
[TestMethod]
public void CheckedDouble()
{
checked
{
double x = double.MaxValue;
x *= 2;
}
}
}
}