The other day, I played around with the Fibonacci sequence and overflowed my
int
variables in surprisingly low iterations. Did you know you’ll overflow an integer in **48
** iterations? Don’t believe me? Let’s take a look at the result of the code you’ll see later in this post.
Enter an integer: 48
Fibonacci sequence for 48:
0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,
317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580
141,267914296,433494437,701408733,1134903170,1836311903,-1323752223
You’ll notice that the last value has looped back to a negative value. Oh no! This is a tell-tale sign that an overflow has occurred.
This post will explore how to keep your applications safe from these overflow issues.
The code causing the overflow
First, let’s look at my implementation of the Fibonacci sequence generator. I’m using
Spectre.Console
to make the output more appealing, but it’s optional. I’m also using the
System.Numerics
namespace, which will allow you to experiment with different number types in .NET.
using System.Numerics;
using Spectre.Console;
while (true)
{
try
{
var integer = AnsiConsole.Ask<int>("Enter an integer: ");
if (integer <= -1)
{
AnsiConsole.MarkupLine("Goodbye!");
break;
}
var numbers = GenerateFibonacci<int>((uint)integer);
AnsiConsole.MarkupLine($"[bold green]Fibonacci sequence for {integer}:[/]");
AnsiConsole.MarkupLine($"[bold yellow]{string.Join(",", numbers)}[/]");
AnsiConsole.MarkupLine("");
}
catch (ArgumentOutOfRangeException)
{
AnsiConsole.MarkupLine("[red]Error: pick a value greater than 2[/]");
}
}
static T[] GenerateFibonacci<T>(uint iterations)
where T : INumber<T>
{
ArgumentOutOfRangeException
.ThrowIfLessThan<uint>(iterations, 2, "iterations must be greater than or equal to 2");
T[] fib = new T[iterations];
fib[0] = T.Zero;
fib[1] = T.One;
for (int i = 2; i < iterations; i++)
{
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib;
}
Neat! Now, why does the issue occur in the first place?
Unchecked Arithmetic
By default, .NET performs
unchecked
arithmetic operations. That means the runtime assumes you know what you’re doing when adding one variable to another. This is true in most day-to-day cases, as the scale of numbers most developers deal with is on the lower side of the maximum
int
and long
values.
For integers, the max value is 2147483647
, and it is doubled to 4294967295
if you drop the sign
—/+
from the value. For a long, the max value is
9223372036854775807
, which is quite a lot bigger but not infinite. If your application increments values indefinitely, you will run into overflow issues, and no known type will save you. It’s just math!
As you’ve seen previously, the issue is that the numbers will loop around from positive to negative values and back. Your application will continue to “work” but likely will be nonsensical and wrong.
How do we fix this issue? Well, there are two ways.
The checked
keyword
The first and most precise way to fix this issue is to use the
checked
keyword. Using this keyword creates a scope where arithmetic overflows will trigger an OverflowException
.
static T[] GenerateFibonacci<T>(uint iterations)
where T : INumber<T>
{
ArgumentOutOfRangeException
.ThrowIfLessThan<uint>(iterations, 2, "iterations must be greater than or equal to 2");
T[] fib = new T[iterations];
fib[0] = T.Zero;
fib[1] = T.One;
for (int i = 2; i < iterations; i++)
{
checked
{
fib[i] = fib[i - 1] + fib[i - 2];
}
}
return fib;
}
Rerunning the same code results in the following exception.
Enter an integer: 49
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
at System.Int32.System.Numerics.IAdditionOperators<System.Int32,System.Int32,System.Int32>.op_CheckedAddition(Int32 left, Int32 right)
at Program.<<Main>$>g__GenerateFibonacci|0_0[T](UInt32 iterations) in /Users/khalidabuhakmeh/RiderProjects/zed-sample/Program.cs:line 40
at Program.<Main>$(String[] args) in /Users/khalidabuhakmeh/RiderProjects/zed-sample/Program.cs:line 16
Nice. This technique is very selective and changes the intermediate language to call the
op_CheckedAddition
method instead.
// [40 13 - 40 46]
IL_003f: ldloc.0 // fib
IL_0040: ldloc.1 // i
IL_0041: ldloc.0 // fib
IL_0042: ldloc.1 // i
IL_0043: ldc.i4.1
IL_0044: sub.ovf
IL_0045: ldelem !!0/*T*/
IL_004a: ldloc.0 // fib
IL_004b: ldloc.1 // i
IL_004c: ldc.i4.2
IL_004d: sub.ovf
IL_004e: ldelem !!0/*T*/
IL_0053: constrained. !!0/*T*/
IL_0059: call !2/*T*/ class [System.Runtime]System.Numerics.IAdditionOperators`3<!!0/*T*/, !!0/*T*/, !!0/*T*/>::op_CheckedAddition(!0/*T*/, !1/*T*/)
IL_005e: stelem !!0/*T*/
Cool! Problem solved, but what if we want all our arithmetic checked?
Project Setting for Checked Arithmetic
You can add the
CheckForOverflowUnderflow
property in your project settings, and it will change all supported arithmetic operations to use checked operations.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.49.1" />
</ItemGroup>
</Project>
Easy peasy. Running the original code results in the same System.OverflowException
.
To opt out of checking, you can use the converse unchecked
keyword.
static T[] GenerateFibonacci<T>(uint iterations)
where T : INumber<T>
{
ArgumentOutOfRangeException
.ThrowIfLessThan<uint>(iterations, 2, "iterations must be greater than or equal to 2");
T[] fib = new T[iterations];
fib[0] = T.Zero;
fib[1] = T.One;
for (int i = 2; i < iterations; i++)
{
unchecked
{
fib[i] = fib[i - 1] + fib[i - 2];
}
}
return fib;
}
Conclusion
We can sometimes take for granted arithmetic operations and assume they will work. In most cases, they will, but if you’re building mission-critical applications that could run into underflow and overflow situations, it’s better to check these values so as not to cause potential headaches down the line.
I hope you enjoyed this post, and as always, thanks for reading.