奇怪的表现行为

时间:2015-08-18 11:25:10

标签: c# performance cil

所以我有这两种方法,假设将1000项长整数数乘以2。 第一种方法:

[MethodImpl(MethodImplOptions.NoOptimization)]
Power(int[] arr)
{
    for (int i = 0; i < arr.Length; i++)
    {
        arr[i] = arr[i] + arr[i];
    }
}

第二种方法:

[MethodImpl(MethodImplOptions.NoOptimization)]
PowerNoLoop(int[] arr)
{
    int i = 0;
    arr[i] = arr[i] + arr[i];
    i++;
    arr[i] = arr[i] + arr[i];
    i++;
    arr[i] = arr[i] + arr[i];
    i++;
    ............1000 Times........
    arr[i] = arr[i] + arr[i];
}

请注意,我仅将此代码用于性能研究,这就是为什么它看起来如此恶心。

令人惊讶的结果是PowerPowerNoLoop快了近50%,尽管我检查了它们的反编译IL来源和{{1}的内容}}循环与for中的每一行完全相同。 怎么会这样?

4 个答案:

答案 0 :(得分:12)

从我的机器进行样本测量,运行测试10次,PowerNoLoop是第一次:

00:00:00.0277138 00:00:00.0001553
00:00:00.0000142 00:00:00.0000057
00:00:00.0000106 00:00:00.0000053
00:00:00.0000084 00:00:00.0000053
00:00:00.0000080 00:00:00.0000053
00:00:00.0000075 00:00:00.0000053
00:00:00.0000080 00:00:00.0000057
00:00:00.0000080 00:00:00.0000053
00:00:00.0000080 00:00:00.0000053
00:00:00.0000075 00:00:00.0000053

是的,慢了约50%。值得注意的是第一次通过测试时的抖动开销,显然它会烧掉更多核心,试图获得编译的巨大方法。请记住,当您不禁用优化器时,测量会有很大的不同,因此无环路版本会慢大约800%。

总是寻找解释的第一个地方是生成的机器代码,你可以通过Debug&gt;看到它。 Windows&gt;拆卸。主要的麻烦是PowerNoLoop()方法的序幕。在x86代码中看起来像这样:

067E0048  push        ebp                       ; setup stack frame
067E0049  mov         ebp,esp  
067E004B  push        edi                       ; preserve registers
067E004C  push        esi  
067E004D  sub         esp,0FA8h                 ; stack frame size = 4008 bytes  
067E0053  mov         esi,ecx  
067E0055  lea         edi,[ebp-0ACCh]           ; temp2 variables
067E005B  mov         ecx,2B1h                  ; initialize 2756 bytes
067E0060  xor         eax,eax                   ; set them to 0
067E0062  rep stos    dword ptr es:[edi] 

请注意非常大的堆栈大小,4008字节。对于只有一个局部变量的方法来说,它应该只需要8个字节。额外的4000个是临时变量,我将它们命名为temp2。它们被rep stos指令初始化为0,需要一段时间。我无法解释2756。

在非优化代码中,个人添加是非常沉重的事情。我将为您节省机器代码转储并将其写入等效的C#代码中:

if (i >= arr.Length) goto throwOutOfBoundsException
var temp1 = arr[i];
if (i >= arr.Length) goto throwOutOfBoundsException
var temp2 = temp1 + arr[i];
if (i >= arr.Length) goto throwOutOfBoundsException
arr[i] = temp2

一遍又一遍地重复,总共一千次。 temp2变量是麻烦制造者,每个单独的陈述都有一个。因此向堆栈帧大小添加4000个字节。如果有人在2756猜测,那么我很乐意在评论中听到它。

在方法开始运行之前必须将它们全部设置为0大致是产生50%减速的原因。可能还有一些指令获取和解码开销,它不能轻易地从测量中分离出来。

值得注意的是,当您删除[MethodImpl]属性并允许优化器完成其工作时,它们被消除。事实上,该方法根本没有优化,当然因为它不想处理如此大量的代码。

结论你应该绘制的是始终将它留给抖动优化器来为你展开循环。它知道的更好。

答案 1 :(得分:2)

Hans Passant似乎已经触及了主要问题,但却错过了一些观点。

首先正如Mark Jansen所说,代码生成器(在JIT中)有一个特殊情况,可以在简单的for循环中删除对简单数组访问的绑定检查。 [MethodImpl(MethodImplOptions.NoOptimization)]很可能不会影响这一点。你的展开循环必须检查3000次!

下一个问题是从内存中读取数据(或代码)需要更长的时间,然后运行已经在处理器1级缓存中的指令。从CPU到RAM的带宽也是有限的,因此每当CPU从内存中读取指令时,它就无法读取(或更新)数组。一旦Power中的循环第一次执行,所有处理器指令都将位于第一级缓存中 - 它们甚至可以以部分解码的形式存储。

更新1000个不同的tempN变量,会将负载放在CPU缓存甚至RAM上(因为CPU不知道它们不会被再次读取,所以必须将它们保存到RAM中)(不使用{ {1}},JIT可以将MethodImplOptions.NoOptimization变量合并到一些变量中,然后这些变量将适合寄存器。)

现在大多数CPU可以同时运行许多指令(Superscalar),因此很可能同时执行所有循环检查(1&lt; arr.Length)等。来自阵列的存储/加载。即使是循环结束时的条件GoTo也会被Speculative execution(和/或Out-of-order execution)隐藏。

任何合理的CPU都可以在大约从内存中读取/写入值的时间内运行循环。

如果您在20年前在PC上完成了相同的测试,那么很可能您会获得预期的结果。

答案 2 :(得分:1)

因为c#jit编译器被优化以消除边界检查,如果它可以推断出变量不会超出for循环的范围。

for (int i = 0; i < arr.Length; i++)的情况被优化器捕获,另一种情况则没有。

这是一篇关于它的博客文章,它很长但值得一读:http://blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds-check-elimination-in-the-clr.aspx

答案 3 :(得分:1)

我在测试中没有看到这些结果。我怀疑你的测试可能因垃圾收集而失真。

我的发布版本测试结果如下(使用Visual Studio 2015,.Net 4.6,Windows 10):

64:

Power() took 00:00:01.5277909
PowerNoLoop() took 00:00:01.4462461
Power() took 00:00:01.5403739
PowerNoLoop() took 00:00:01.4038312
Power() took 00:00:01.5327902
PowerNoLoop() took 00:00:01.4318121
Power() took 00:00:01.5451933
PowerNoLoop() took 00:00:01.4252743

86:

Power() took 00:00:01.1769501
PowerNoLoop() took 00:00:00.9933677
Power() took 00:00:01.1557201
PowerNoLoop() took 00:00:01.0033348
Power() took 00:00:01.1119558
PowerNoLoop() took 00:00:00.9588702
Power() took 00:00:01.1167853
PowerNoLoop() took 00:00:00.9553292

代码:

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace ConsoleApplication1
{
    internal class Program
    {
        private static void Main()
        {
            Stopwatch sw = new Stopwatch();

            int count = 200000;
            var test = new int[1000];

            for (int trial = 0; trial < 4; ++trial)
            {
                sw.Restart();

                for (int i = 0; i < count; ++i)
                    Power(test);

                Console.WriteLine("Power() took " + sw.Elapsed);
                sw.Restart();

                for (int i = 0; i < count; ++i)
                    PowerNoLoop(test);

                Console.WriteLine("PowerNoLoop() took " + sw.Elapsed);
            }
        }

        [MethodImpl(MethodImplOptions.NoOptimization)]
        public static void Power(int[] arr)
        {
            for (int i = 0; i < arr.Length; i++)
            {
                arr[i] = arr[i] + arr[i];
            }
        }

        [MethodImpl(MethodImplOptions.NoOptimization)]
        public static void PowerNoLoop(int[] arr)
        {
            int i = 0;
            arr[i] = arr[i] + arr[i];
            ++i;
            <snip> Previous two lines repeated 1000 times.
        }
    }
}