在 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 秒。
我有以下 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 秒。