在 64 位下执行缓慢。可能是 RyuJIT 错误?

Slow execution under 64 bits. Possible RyuJIT bug?

我有以下 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 块,它也会在 0.6 秒 中使用 64 位的 VS 2015 运行s。

当有 try catch 块时,这看起来像是严重的 RyuJIT 回归。我说得对吗?

基准测试是一门艺术。对您的代码做一个小修改:

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

现在您会发现差异消失了。或者换句话说,x86 版本现在和 x64 代码一样慢。您可能可以从这个小改动中找出 RyuJIT 没有做什么遗留抖动所做的事情,它并没有消除不必要的

   sum += i;

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

称其为错误,嗯,不是真的。编写合理的代码,抖动不会让您失望。优化确实永远从通常的地方开始,在程序员的耳朵之间。

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

让我们删除多余的 throw:

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

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

让我们将计数器更改为实际的 int 值:

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

使用此代码,64 位版本的执行时间从 4 秒减少到大约 1.7 秒。仍然是 32 位版本的两倍。但是我认为这很有趣。不幸的是,在我快速 Google 搜索之后,我还没有找到原因,但如果我发现发生这种情况的原因,我会深入挖掘并更新这个答案。

至于我们想要削减 64 位版本的剩余秒数,我可以看出这是将 for 中的 sum 增加 i环形。 让我们更改它,以便 sum 不超出其范围:

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

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

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

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

我在这里推理;所以不要把这当作福音。我只是想 post 我的发现。我相信其他人能够阐明我发现的问题。

--

更新

@HansPassant 的回答指出 sum += i; 行可能会被删除,因为它被认为是不必要的,这是完全有道理的, sum 没有在 [=21= 之外使用] 环形。在他在 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,它在 for 循环中被赋予了 sum 的值。 x 的值没有写到控制台。 sum 不会离开 for 循环。不管你信不信,这实际上将 x64 的执行时间减少到 0.7 秒。但是,x86 版本跳到 1.4 秒。