代码:
internal class Program
{
private static void Main(string[] args)
{
const int iterCount = 999999999;
var sum1 = 0;
var sum2 = 0;
using (new Dis())
{
var sw = DateTime.Now;
for (var i = 0; i < iterCount; i++)
sum1 += i;
Console.WriteLine(sum1);
Console.WriteLine(DateTime.Now - sw);
}
using (new Dis())
{
var sw = DateTime.Now;
for (var i = 0; i < iterCount; i++)
sum2 += i;
Console.WriteLine(sum2);
Console.WriteLine(DateTime.Now - sw);
}
Console.ReadLine();
}
private class Dis : IDisposable
{
public void Dispose(){}
}
}
相同使用的两个相同的块。
输出:
2051657985
00:00:00.3690996
2051657985
00:00:02.2640266
第二块需要2.2秒!但是如果要摆脱使用,持续时间变得相同(~0.3秒,就像第一个一样)。 我试过.net framework 4.5和.net core 1.1,在发布中,结果是一样的。
有人可以解释这种行为吗?
答案 0 :(得分:12)
您必须查看抖动生成的机器代码以查看根本原因。使用工具&gt;选项&gt;调试&gt;一般&gt;取消抑制JIT优化选项。切换到发布版本。在第一个和第二个循环上设置断点。当它命中时使用Debug&gt; Windows&gt;拆卸。
您将看到for循环体的机器代码:
sum1 += i;
00000035 add esi,eax
和
sum2 += i;
000000d9 add dword ptr [ebp-24h],eax
或者换句话说,sum1
变量存储在CPU寄存器esi
中。但sum2
变量存储在内存中,位于方法的堆栈帧上。巨大的,巨大的差异。寄存器非常快,内存很慢。堆栈帧的内存将位于L1缓存中,在访问该缓存的现代机器上具有3个周期的延迟。存储缓冲区很快就会被大量写入所淹没,导致处理器停止运行。
找到一种在CPU寄存器中保存变量的方法是one of the primary jitter optimization duties。但这有局限性,特别是x86几乎没有可用的寄存器。当它们全部用完时,抖动没有选择,只能使用内存。请注意,using
语句在引擎盖下有一个额外的隐藏局部变量,这就是它产生影响的原因。
理想情况下,抖动优化器可以更好地选择如何分配寄存器。将它们用于循环变量(它所做的)和和变量。一个提前编译器可以做到这一点,有足够的时间来执行代码分析。但是,即时编译器在严格的时间限制下运行。
基本对策是:
最后一个子弹对遗留x64抖动(目标.NET 3.5使用它)有效,但不适用于4.6中首次提供的x64抖动重写(又名RYuJIT)。重写是必要的,因为遗留抖动花费了太多时间来优化代码。令人失望的是,RyuJIT确实有令人失望的诀窍,我认为它的优化者可以在这里做得更好。