6

我有以下 C# 代码试图在发布模式下进行基准测试:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication54
{
class Program
{
    static void Main(string[] args)
    {
        int counter = 0;
        var sw = new Stopwatch();
        unchecked
        {
            int sum = 0;
            while (true)
            {
                try
                {
                    if (counter > 20)
                        throw new Exception("exception");
                }
                catch
                {
                }

                sw.Restart();
                for (int i = 0; i < int.MaxValue; i++)
                {
                    sum += i;
                }
                counter++;
                Console.WriteLine(sw.Elapsed);
            }

        }
    }
}
}

我在 64 位机器上并安装了 VS 2015。当我在 32 位下运行代码时,它每次迭代运行大约0.6 秒,并打印到控制台。当我在 64 位下运行它时,每次迭代的持续时间只会跳到4 秒!我在只安装了 VS 2013 的同事计算机上尝试了示例代码。32 位和 64 位版本的运行时间都在0.6 秒左右

除此之外,如果我们只删除 try catch 块,它在 64 位的 VS 2015 中也能在0.6 秒内运行。

当有一个 try catch 块时,这看起来像是一个严重的 RyuJIT 回归。我对么 ?

4

2 回答 2

11

基准测试是一门艺术。对您的代码进行一些小修改:

   Console.WriteLine("{0}", sw.Elapsed, sum);

现在你会看到差异消失了。或者换句话说,x86 版本现在和 x64 代码一样慢。您可能会从这个微小的变化中找出 RyuJIT 没有做的传统抖动所做的事情,它并没有消除不必要的

   sum += i;

当您使用 Debug > Windows > Disassembly 查看生成的机器代码时,您可以看到一些东西。这确实是 RyuJIT 的一个怪癖。它的死代码消除不如传统抖动那么彻底。否则,并非完全没有理由,Microsoft 重写了 x64 抖动,因为它无法轻松修复错误。其中一个是优化器的一个相当讨厌的问题,它对优化方法所花费的时间没有上限。在具有非常大的主体的方法上导致相当糟糕的行为,它可能会在树林中出现数十毫秒并导致明显的执行暂停。

称它为错误,嗯,不是真的。编写理智的代码,抖动不会让你失望。优化确实永远从程序员耳中的通常位置开始。

于 2015-11-18T08:22:19.930 回答
0

经过一些测试,我得到了一些有趣的结果。我的测试围绕着try catch街区展开。正如 OP 指出的那样,如果删除此块,则执行时间是相同的。我进一步缩小了这个范围,并得出结论,这是因为块中的语句中的counter变量。iftry

让我们删除多余的throw

                try
                {
                    if (counter== 0) { }
                }
                catch
                {
                }

使用此代码,您将获得与使用原始代码相同的结果。

让我们将 counter 更改为实际的 int 值:

                try
                {
                    if (1 == 0) { }
                }
                catch
                {
                }

使用此代码,64 位版本的执行时间从 4 秒减少到约 1.7 秒。仍然是 32 位版本的两倍。不过我觉得这很有趣。不幸的是,在我快速的谷歌搜索之后,我还没有想出一个理由,但如果我发现为什么会发生这种情况,我会进一步挖掘并更新这个答案。

至于我们想要削减 64 位版本的剩余时间,我可以看到这取决于在循环中增加sumby 。让我们改变它,使其不超过其界限:iforsum

            for (int i = 0; i < int.MaxValue; i++)
            {
                sum ++;
            }

此更改(连同try块中的更改)会将 64 位应用程序的执行时间减少到 0.7 秒。我对 1 秒时间差异的推理是由于 64 位版本需要处理int自然是 32 位的人为方式。

在 32 位版本中,有 32 位分配给 Int32 ( sum)。当sum超出其界限时,很容易确定这一事实。

在 64 位版本中,有 64 位分配给 Int32 ( sum)。当总和超过其界限时,需要一种机制来检测这一点,这可能会导致速度变慢。由于分配的冗余位增加,甚至添加sum&的操作也可能需要更长的时间。i

我在这里进行理论化;所以不要把这当成福音。我只是想我会发布我的发现。我相信其他人将能够对我发现的问题有所了解。

--

更新

@HansPassant 的回答指出,该sum += i;行可能被删除,因为它被认为是不必要的,这是完全有道理的,sum没有在for循环之外使用。在他在 for 循环之外引入 sum 的值后,我们注意到 x86 版本与 x64 版本一样慢。所以我决定做一些测试。让我们将 for 循环和打印更改为以下内容:

                int x = 0;
                for (int i = 0; i < int.MaxValue; i++)
                {
                    sum += i;
                    x = sum;
                }
                counter++;
                Console.WriteLine(sw.Elapsed + "  " +  x);

你可以看到我引入了一个新的,它在循环int x中被分配了值。x 的值没有被写出到控制台。不离开循环。不管你信不信,这实际上将 x64 的执行时间减少到了 0.7 秒。但是,x86 版本最多跳到 1.4 秒。sumforsumfor

于 2015-11-18T09:12:45.690 回答