为什么在.NET中检查算术有时比未检查更快?

时间:2014-05-19 11:39:14

标签: c# .net integer-overflow

为什么在C#项目属性>下打开“检查算术下溢/溢出”时呢?构建>高级,以下代码比关闭选项(141毫秒)运行得更快(138毫秒)?

测试运行#1:带校验算术的138ms,没有

的141ms
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);
    }
}

另一方面,如果您注释掉if (i == 1000) i *= 2;,则检查的代码比未检查的代码(116毫秒)运行得慢(120毫秒)。

测试运行#2:带有检查算术的120ms,没有

的116ms
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);
    }
}

进程:重复从PowerShell提示符在Visual Studio外部手动运行.exe,直到生成的时间戳保持一致(±1 ms);多次在设置之间翻转以确保一致的结果。

测试框设置:

  • Windows 8.1 Pro x64
  • VS2013 Update 2
  • Intel Core i7-4500
  • 默认C#控制台项目模板
  • 发布配置

2 个答案:

答案 0 :(得分:7)

答案是你正在处理许多常量,这些常量允许JIT做出安全的假设,它永远不会溢出。如果您使用类似Fibbonacci基准的东西,差异就会变得清晰。

2770ms vs 4150ms(AnyCPU,32位首选)

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

}

答案 1 :(得分:3)

在这种情况下,检查算术比未选中更快,原因有两个:

  • 编译器能够确定检查大部分算法是不必要的,因此检查的额外开销很小。这是一个简单的测试工件。 leppie's answer给出了一个具有实质性差异的算法的一个很好的例子。

  • 为实现检查而插入的代码恰好导致密钥分支目标不会落在对齐边界上。您可以通过两种方式看到这一点:

    • int a = 0;替换为int a = args.Length;。运行测试并观察性能反转消失。原因是附加代码导致分支目标对齐。

    • 检查下面的装配。我通过将Process.EnterDebugMode();Debugger.Break();添加到Main的末尾并从命令行运行Release模式.exe来获取它。注意当检查的代码对i % 35 == 0进行测试时,如果为false,它将分支到00B700CA,这是一个对齐的指令。与未经检查的代码对比,分支到012D00C3。即使已检查的代码有一条额外的jo指令,对齐分支的节省也会超过其成本。

经过

        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  
        }