Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is checked arithmetic in .NET sometimes faster than unckecked?

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)?

Test Run #1: 138ms with checked arithmetic, 141ms without

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).

Test Run #2: 120ms with checked arithmetic, 116ms without

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:

  • Windows 8.1 Pro x64
  • VS2013 Update 2
  • Intel Core i7-4500
  • default C# Console project template
  • Release configuration
like image 574
Edward Brey Avatar asked May 19 '14 11:05

Edward Brey


People also ask

How to handle arithmetic overflow exception in c#?

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.

How to check arithmetic overflow c#?

Go to project properties and find the Build tab, click “Advanced…” and tick the “Check for arithmetic overflow/underflow” box.

What does unchecked do in c#?

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.

Which operator enforces the arithmetic overflow and underflow checking?

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.


2 Answers

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;
    }

}
like image 167
leppie Avatar answered Nov 14 '22 22:11

leppie


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.

Checked

        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  
        }

Unchecked

        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  
        }
like image 28
Edward Brey Avatar answered Nov 14 '22 23:11

Edward Brey