首页 > C# > 当我用这样的 try-catch 块将 for 循环包装在 Fibo() 中时更加快?

当我用这样的 try-catch 块将 for 循环包装在 Fibo() 中时更加快?

上一篇 下一篇

我写了一些代码来测试try-catch的影响,但看到了一些令人惊讶的结果。

static void Main(string[] args)
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;

    long start = 0, stop = 0, elapsed = 0;
    double avg = 0.0;

    long temp = Fibo(1);

    for (int i = 1; i < 100000000; i++)
    {
        start = Stopwatch.GetTimestamp();
        temp = Fibo(100);
        stop = Stopwatch.GetTimestamp();

        elapsed = stop - start;
        avg = avg + ((double)elapsed - avg) / i;
    }

    Console.WriteLine("Elapsed: " + avg);
    Console.ReadKey();
}

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    for (int i = 1; i < n; i++)
    {
        n1 = n2;
        n2 = fibo;
        fibo = n1 + n2;
    }

    return fibo;
}

在我的计算机上,这始终打印出大约 0.96 的值。

当我用这样的 try-catch 块将 for 循环包装在 Fibo() 中时:

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    try
    {
        for (int i = 1; i < n; i++)
        {
            n1 = n2;
            n2 = fibo;
            fibo = n1 + n2;
        }
    }
    catch {}

    return fibo;
}

现在它始终打印出 0.69…——它实际上运行得更快!但是为什么?

注意:我使用发布配置编译了它,并直接运行了EXE文件(在Visual Studio之外)。

编辑:Jon Skeet的出色分析表明,try-catch以某种方式导致x86 CLR在这种特定情况下以更有利的方式使用CPU寄存器(我认为我们还不明白为什么)。我证实了 Jon 的发现,即 x64 CLR 没有这种差异,并且它比 x86 CLR 更快。我还在斐波那契方法中使用类型而不是类型进行了测试,然后 x86 CLR 与 x64 CLR 一样快。intlong


更新:看起来罗斯林已经解决了这个问题。相同的机器,相同的CLR版本 — 使用VS 2013编译时,问题仍然存在,但是当使用VS 2015编译时,问题消失了。

分割线

网友回答:

好吧,你计时的方式对我来说看起来很讨厌。只对整个循环进行计时会更明智:

var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
    Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);

这样你就不会受到微小的时间、浮点运算和累积误差的摆布。

进行该更改后,查看“非捕获”版本是否仍然比“捕获”版本慢。

编辑:好的,我自己试过了 – 我看到了相同的结果。很奇怪。我想知道尝试/捕获是否禁用了一些不好的内联,但使用反而没有帮助……[MethodImpl(MethodImplOptions.NoInlining)]

基本上你需要看看 cordbg 下的优化 JITted 代码,我怀疑……

编辑:更多信息:

  • 将尝试/捕获放在生产线周围仍然可以提高性能,但不如将其放在整个块周围那么多n++;
  • 如果您捕获特定异常(在我的测试中),它仍然很快ArgumentException
  • 如果您在 catch 块中打印异常,它仍然很快
  • 如果您在 catch 块中重新抛出异常,它会再次变慢
  • 如果您使用 finally 块而不是 catch 块,它又变慢了
  • 如果您使用最终块捕获块,则速度很快

奇怪。。。

编辑:好的,我们有拆卸…

这是使用 C# 2 编译器和 .NET 2(32 位)CLR,使用 mdbg 进行反汇编(因为我的机器上没有 cordbg)。即使在调试器下,我仍然看到相同的性能效果。快速版本在变量声明和 return 语句之间的所有内容周围使用一个块,只有一个处理程序。显然,慢速版本是相同的,除了没有尝试/捕获。调用代码(即 Main)在这两种情况下都是相同的,并且具有相同的程序集表示形式(因此这不是内联问题)。trycatch{}

快速版本的反汇编代码:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        edi
 [0004] push        esi
 [0005] push        ebx
 [0006] sub         esp,1Ch
 [0009] xor         eax,eax
 [000b] mov         dword ptr [ebp-20h],eax
 [000e] mov         dword ptr [ebp-1Ch],eax
 [0011] mov         dword ptr [ebp-18h],eax
 [0014] mov         dword ptr [ebp-14h],eax
 [0017] xor         eax,eax
 [0019] mov         dword ptr [ebp-18h],eax
*[001c] mov         esi,1
 [0021] xor         edi,edi
 [0023] mov         dword ptr [ebp-28h],1
 [002a] mov         dword ptr [ebp-24h],0
 [0031] inc         ecx
 [0032] mov         ebx,2
 [0037] cmp         ecx,2
 [003a] jle         00000024
 [003c] mov         eax,esi
 [003e] mov         edx,edi
 [0040] mov         esi,dword ptr [ebp-28h]
 [0043] mov         edi,dword ptr [ebp-24h]
 [0046] add         eax,dword ptr [ebp-28h]
 [0049] adc         edx,dword ptr [ebp-24h]
 [004c] mov         dword ptr [ebp-28h],eax
 [004f] mov         dword ptr [ebp-24h],edx
 [0052] inc         ebx
 [0053] cmp         ebx,ecx
 [0055] jl          FFFFFFE7
 [0057] jmp         00000007
 [0059] call        64571ACB
 [005e] mov         eax,dword ptr [ebp-28h]
 [0061] mov         edx,dword ptr [ebp-24h]
 [0064] lea         esp,[ebp-0Ch]
 [0067] pop         ebx
 [0068] pop         esi
 [0069] pop         edi
 [006a] pop         ebp
 [006b] ret

慢速版本的反汇编代码:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        esi
 [0004] sub         esp,18h
*[0007] mov         dword ptr [ebp-14h],1
 [000e] mov         dword ptr [ebp-10h],0
 [0015] mov         dword ptr [ebp-1Ch],1
 [001c] mov         dword ptr [ebp-18h],0
 [0023] inc         ecx
 [0024] mov         esi,2
 [0029] cmp         ecx,2
 [002c] jle         00000031
 [002e] mov         eax,dword ptr [ebp-14h]
 [0031] mov         edx,dword ptr [ebp-10h]
 [0034] mov         dword ptr [ebp-0Ch],eax
 [0037] mov         dword ptr [ebp-8],edx
 [003a] mov         eax,dword ptr [ebp-1Ch]
 [003d] mov         edx,dword ptr [ebp-18h]
 [0040] mov         dword ptr [ebp-14h],eax
 [0043] mov         dword ptr [ebp-10h],edx
 [0046] mov         eax,dword ptr [ebp-0Ch]
 [0049] mov         edx,dword ptr [ebp-8]
 [004c] add         eax,dword ptr [ebp-1Ch]
 [004f] adc         edx,dword ptr [ebp-18h]
 [0052] mov         dword ptr [ebp-1Ch],eax
 [0055] mov         dword ptr [ebp-18h],edx
 [0058] inc         esi
 [0059] cmp         esi,ecx
 [005b] jl          FFFFFFD3
 [005d] mov         eax,dword ptr [ebp-1Ch]
 [0060] mov         edx,dword ptr [ebp-18h]
 [0063] lea         esp,[ebp-4]
 [0066] pop         esi
 [0067] pop         ebp
 [0068] ret

在每种情况下,都显示了调试器在简单的“单步执行”中输入的位置。*

编辑:好的,我现在已经浏览了代码,我想我可以看到每个版本是如何工作的……我相信较慢的版本更慢,因为它使用更少的寄存器和更多的堆栈空间。对于较小的值,这可能更快 – 但是当循环占用大部分时间时,它就更慢了。n

try/catch 块可能会强制保存和恢复更多的寄存器,因此 JIT 也将它们用于循环……这恰好提高了整体性能。目前尚不清楚 JIT 在“正常”代码中不使用尽可能多的寄存器是否是一个合理的决定。

编辑:刚刚在我的x64机器上尝试过这个。在此代码上,x64 CLR 比 x3 CLR 快得多(大约快 4-86 倍),而在 x64 下,try/catch 块没有明显的区别。

分割线

网友回答:

一位专门了解堆栈使用优化的 Roslyn 工程师看了一下,并向我报告说,C# 编译器生成局部变量存储的方式与 JIT 编译器在相应的 x86 代码中注册调度的方式之间的交互似乎存在问题。结果是在局部变量的加载和存储上生成次优代码。

出于某种原因,我们所有人都不清楚,当 JITter 知道块位于 try 保护区域中时,可以避免有问题的代码生成路径。

这很奇怪。我们将跟进 JITter 团队,看看我们是否可以输入错误,以便他们修复此问题。

此外,我们还致力于改进 Roslyn 对 C# 和 VB 编译器算法的改进,以确定何时可以将局部变量设置为“临时”,也就是说,只是在堆栈上推送和弹出,而不是在激活期间在堆栈上分配特定位置。我们相信,如果我们给它更好的提示,说明当地人何时可以更早地“死亡”,JITter将能够更好地完成寄存器分配。

感谢您提请我们注意这一点,并为奇怪的行为道歉。

分割线

网友回答:

Jon 的反汇编表明,两个版本之间的区别在于快速版本使用一对寄存器 () 来存储慢速版本没有的局部变量之一。esi,edi

JIT 编译器对包含 try-catch 块的代码与不包含 try-catch 块的代码的寄存器使用做出不同的假设。这会导致它做出不同的寄存器分配选择。在这种情况下,这有利于带有 try-catch 块的代码。不同的代码可能会导致相反的效果,所以我不会将其视为通用加速技术。

最后,很难判断哪个代码最终会运行得最快。像寄存器分配和影响它的因素是低级的实现细节,我看不出任何特定技术如何可靠地生成更快的代码。

例如,请考虑以下两种方法。它们改编自一个现实生活中的例子:

interface IIndexed { int this[int index] { get; set; } }
struct StructArray : IIndexed { 
    public int[] Array;
    public int this[int index] {
        get { return Array[index]; }
        set { Array[index] = value; }
    }
}

static int Generic<T>(int length, T a, T b) where T : IIndexed {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}
static int Specialized(int length, StructArray a, StructArray b) {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}

一个是另一个的通用版本。将泛型类型替换为将使方法相同。因为是值类型,所以它获取自己的泛型方法的编译版本。然而,实际运行时间明显长于专用方法,但仅适用于 x86。对于 x64,时间几乎相同。在其他情况下,我也观察到 x64 的差异。StructArrayStructArray

模板简介:该模板名称为【当我用这样的 try-catch 块将 for 循环包装在 Fibo() 中时更加快?】,大小是暂无信息,文档格式为.编程语言,推荐使用Sublime/Dreamweaver/HBuilder打开,作品中的图片,文字等数据均可修改,图片请在作品中选中图片替换即可,文字修改直接点击文字修改即可,您也可以新增或修改作品中的内容,该模板来自用户分享,如有侵权行为请联系网站客服处理。欢迎来懒人模板【C#】栏目查找您需要的精美模板。

相关搜索
  • 下载密码 lanrenmb
  • 下载次数 299次
  • 使用软件 Sublime/Dreamweaver/HBuilder
  • 文件格式 编程语言
  • 文件大小 暂无信息
  • 上传时间 02-10
  • 作者 网友投稿
  • 肖像权 人物画像及字体仅供参考
栏目分类 更多 >
热门推荐 更多 >
微信文章 微信模板 微信图片 企业网站 html5 自适应 响应式 微信素材 单页式简历模板 微信公众平台
您可能会喜欢的其他模板