对于.NET x64中的循环性能异常:偶数迭代相关性

本文关键字:异常 相关性 迭代 性能 循环性 x64 NET 循环 对于 | 更新日期: 2023-09-27 18:27:15

运行一个带有大量迭代的空for循环,我得到了运行所需时间的不同数字:

public static class Program
{
    static void Main()
    {
        var sw = new Stopwatch();
        sw.Start();
        for (var i = 0; i < 1000000000; ++i)
        {
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);
    }
}

以上操作将在我的机器上运行约200ms,但如果我将其增加到1000000001,则需要4x的时间!如果我达到1000000002,那么它又降到200ms!

这种似乎发生在偶数次迭代中。如果我使用for (var i = 1; i < 1000000001(注意从1而不是0开始),那么它是200ms。或者,如果我做i <= 1000000001(注意小于或等于),那么它是200ms。或(var i = 0; i < 2000000000; i += 2)

这似乎仅适用于x64,但适用于(至少)4.0之前的所有.NET版本。此外,它仅在调试器分离的发布模式下显示。

UPDATE我认为这可能是由于jit中的一些巧妙的位转换,但以下内容似乎证明了这一点:如果你在循环中创建一个对象,那么也需要4倍的时间:

public static class Program
{
    static void Main()
    {
        var sw = new Stopwatch();
        sw.Start();
        object o = null;
        for (var i = 0; i < 1000000000; i++)
        {
            o = new object();
        }
        sw.Stop();
        Console.WriteLine(o); // use o so the compiler won't optimize it out
        Console.WriteLine(sw.ElapsedMilliseconds);
    }
}

这在我的机器上大约需要1秒,但增加1到1000000001需要4秒。这是一个额外的3000ms,所以这不可能真的是由于比特移位,因为在最初的问题中,这也会显示为3000ms的差异。

对于.NET x64中的循环性能异常:偶数迭代相关性

以下是反汇编:

00000031  xor         eax,eax 
  for (var i = 0; i < 1000000001; ++i)
00000033  inc         eax           
00000035  cmp         eax,3B9ACA01h 
0000003a  jl          0000000000000033 
0000003c  movzx       eax,byte ptr [rbx+18h] 
00000040  test        eax,eax 
00000042  je          0000000000000073 

00000031  xor         eax,eax 
     for (var i = 0; i < 1000000000; ++i)
00000033  add         eax,4 
00000036  cmp         eax,3B9ACA00h 
0000003b  jl          0000000000000033 
0000003d  movzx       eax,byte ptr [rbx+18h] 
00000041  test        eax,eax 
00000043  je          0000000000000074 

我看到的唯一区别是,在偶数循环中,循环索引一次增加4(add eax 4),而不是一次增加1(inc eax),因此它以4倍的速度完成循环。

这只是猜测,但我相信这是在把循环扩大4倍。因此,它将物体放置在循环内4次,并且增量仅快4倍。但因为身体是空的,空身体乘以4仍然是空的。你从循环展开中获得的增益比你预期的要大得多。