Why is it when I turn on "check for arithmetic underflow/overflow" under C# Project Properties > Build > Advanced, that the following code runs faster (138 ms) than if I turn the option off (141 ms)?
using System;
using System.Diagnostics;
class Program
{
static void Main(string[] args)
{
var s = new Stopwatch();
s.Start();
int a = 0;
for (int i = 0; i < 100000000; i += 3) {
if (i == 1000)
i *= 2;
if (i % 35 == 0)
++a;
}
s.Stop();
Console.WriteLine(s.ElapsedMilliseconds);
Console.WriteLine(a);
}
}
On the other hand, if you comment out the if (i == 1000) i *= 2;
, then the checked code runs slower (120 ms) than the unchecked code (116 ms).
using System;
using System.Diagnostics;
class Program
{
static void Main(string[] args)
{
var s = new Stopwatch();
s.Start();
int a = 0;
for (int i = 0; i < 100000000; i += 3) {
if (i % 35 == 0)
++a;
}
s.Stop();
Console.WriteLine(s.ElapsedMilliseconds);
Console.WriteLine(a);
}
}
Process: Manually ran .exe outside Visual Studio from PowerShell prompt repeatedly until resulting timestamp was consistent (± 1 ms); flipped between settings multiple times to ensure consistent results.
Test Box Setup:
If you want to ensure that arithmetic operations will throw overflow exceptions if an overflow happens, you need to use the checked { ... } code block. When using the checked { ... } code block, if any arithmetic operation causes an overflow, an OverflowException will be thrown, and will need to be catched and handled.
Go to project properties and find the Build tab, click “Advanced…” and tick the “Check for arithmetic overflow/underflow” box.
The unchecked keyword is used to suppress overflow-checking for integral-type arithmetic operations and conversions. In an unchecked context, if an expression produces a value that is outside the range of the destination type, the overflow isn't flagged.
Checked operators are used to detect overflow errors that can occur at run time for arithmetic operations that result in too of a large number for the number of bits allocated to the data type of the result in use.
The answer is that you are dealing with many constants that allows the JIT to make safe assumptions that it will never overflow. If you use something like a Fibbonacci benchmark, the difference becomes clear.
2770ms vs 4150ms (AnyCPU, 32-bit prefered)
using System;
using System.Diagnostics;
class Program
{
static void Main(string[] args)
{
var s = new Stopwatch();
s.Start();
int a = 0;
for (int i = 0; i < 100000000; i++)
{
a = Fibonacci(45);
}
s.Stop();
Console.WriteLine(s.ElapsedMilliseconds);
}
public static int Fibonacci(int n)
{
int a = 0;
int b = 1;
for (int i = 0; i < n; i++)
{
int temp = a;
a = b;
// if the JIT compiler is clever, only this one needs to be 'checked'
b = temp + b;
}
return a;
}
}
In this case, checked arithmetic is faster than unchecked for two reasons:
The compiler was able to determine that checking most of the arithmetic was unnecessary, and so the added overhead for checking was slight. This was an artifact of the simplicity of the test. leppie's answer gives a good example of an algorithm with a substantial perf difference.
The code inserted to implement checking on happened to cause a key branch destination to not fall on an alignment boundary. You can see this in two ways:
Replace int a = 0;
with int a = args.Length;
. Run the tests and observe that the performance inversion is gone. The reason is that the additional code causes the branch destination to be aligned.
Examine the assembly below. I obtained it by adding Process.EnterDebugMode();
and Debugger.Break();
to the end of Main
, and running the Release mode .exe from the command line. Notice how when the checked code does the test for i % 35 == 0
, if false, it branches to 00B700CA, which is an aligned instruction. Contrast that with the unchecked code, which branches to 012D00C3. Even though the checked code has an additional jo
instruction, its cost is outweighed by the savings of the aligned branch.
int a = 0;
00B700A6 xor ebx,ebx
for (int i = 0; i < 100000000; i += 3) {
00B700A8 xor esi,esi
if (i == 1000)
00B700AA cmp esi,3E8h
00B700B0 jne 00B700B7
i *= 2;
00B700B2 mov esi,7D0h
if (i % 35 == 0)
00B700B7 mov eax,esi
00B700B9 mov ecx,23h
00B700BE cdq
00B700BF idiv eax,ecx
00B700C1 test edx,edx
00B700C3 jne 00B700CA
++a;
00B700C5 add ebx,1
00B700C8 jo 00B70128
for (int i = 0; i < 100000000; i += 3) {
00B700CA add esi,3
00B700CD jo 00B70128
00B700CF cmp esi,5F5E100h
00B700D5 jl 00B700AA
}
int a = 0;
012D00A6 xor ebx,ebx
for (int i = 0; i < 100000000; i += 3) {
012D00A8 xor esi,esi
if (i == 1000)
012D00AA cmp esi,3E8h
012D00B0 jne 012D00B4
i *= 2;
012D00B2 add esi,esi
if (i % 35 == 0)
012D00B4 mov eax,esi
012D00B6 mov ecx,23h
012D00BB cdq
012D00BC idiv eax,ecx
012D00BE test edx,edx
012D00C0 jne 012D00C3
++a;
012D00C2 inc ebx
for (int i = 0; i < 100000000; i += 3) {
012D00C3 add esi,3
012D00C6 cmp esi,5F5E100h
012D00CC jl 012D00AA
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With