相同代码块执行的持续时间如此不同的原因是什么?

时间:2017-08-29 10:24:36

标签: c# .net .net-core

代码:

 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,在发布中,结果是一样的。

有人可以解释这种行为吗?

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语句在引擎盖下有一个额外的隐藏局部变量,这就是它产生影响的原因。

理想情况下,抖动优化器可以更好地选择如何分配寄存器。将它们用于循环变量(它所做的)和和变量。一个提前编译器可以做到这一点,有足够的时间来执行代码分析。但是,即时编译器在严格的时间限制下运行。

基本对策是:

  • 将代码分解为单独的方法,以便可以重用ESI这样的寄存器。
  • 删除抖动强制(项目&gt;属性&gt;构建选项卡&gt;取消勾选“首选32位”)。 x64提供了8个额外的寄存器。

最后一个子弹对遗留x64抖动(目标.NET 3.5使用它)有效,但不适用于4.6中首次提供的x64抖动重写(又名RYuJIT)。重写是必要的,因为遗留抖动花费了太多时间来优化代码。令人失望的是,RyuJIT确实有令人失望的诀窍,我认为它的优化者可以在这里做得更好。