一些简单的代码行如何显着影响下一代码的执行时间?

时间:2016-04-03 06:34:37

标签: c# .net optimization performance-testing jit

编辑:根据工具>给出的评论,也会附加这两种情况的组合。选项>调试> '抑制模块负载的JIT优化' >取消选中(清除此选项,即使在调试模式下也可以进行JIT优化)。可以看出,伪代码可用时生成的机器代码不好! 在快速版本中,代码直接使用CPU Registers ,但在其他版本中,它在每次迭代中从内存加载p1.x和p1.y!我不知道我们怎么能控制这种行为呢!

当我试图测量用于性能测试的代码块的执行时间时,我发现一些让我困惑的不规则性:如果我在实际测量循环之前添加一些(伪)代码块,则总耗用时间会增加在我的2 GHz PC中,一个很大的因素(从120毫秒到220毫秒,即慢约1.8倍!)。 请注意,在实际情况中,这些伪代码是初始化或类似目的所需的一些不同代码。

注意:我在发布模式中构建应用并通过启动而不调试(Ctrl + F5)运行它以确保所有优化都是用于获得最佳效果。

我在这里展示了一个简化的场景示例:

void test()
{
    Point p1 = new Point(1, 2);
    Point p = p1;
    //*** !!! Comment & UnComment the following 2 lines to see difference: ***
    p.Offset(p1); p.Offset(p1); p.Offset(p1); p.Offset(p1);
    p.Offset(p1); p.Offset(p1); p.Offset(p1); p.Offset(p1);

    Stopwatch timer = new Stopwatch();
    double dt = -1;

    for (int repeat = 1; repeat <= 5; repeat++)
    {
        p = p1; //here we reset the initial p

        timer.Restart();
        for (int i = 0; i < 50000000; i++)
        {
            p.Offset(p1);
        }
        timer.Stop();
        dt = timer.ElapsedMilliseconds;

        textBox1.Text += repeat + "] " + dt + ", p: " + p + "\r\n";
        Application.DoEvents(); if (this.IsDisposed) break;
    }
}

private void button1_Click(object sender, EventArgs e)
{
    test();
}

此问题阻止我比较我需要优化的结果。 顺便说一下,这可能与struct类型(?)有关。

你知道是什么原因以及如何解决它?

编辑(4月4日):我检查过,发现这种行为只发生在struct类型而非class类型上。而且正如我们所知,结构通常是Stack中的值类型而不是堆。这可能在这里有作用,但我不知道..

另一个注意事项:我还发现如果我取消选中“优化代码”&#39;项目属性中的选项&gt;构建选项卡,然后代码实际运行得更快!

两种情况的屏幕截图如下所示: Run Screenshot

拆卸: dis-assembly Screenshot

1 个答案:

答案 0 :(得分:1)

有趣的问题。

在这些情况下,汇编程序总是告诉你真相(而不是CIL,它将完全相同!)。可能是你的东西触发了一个边界条件,它改变了最内层循环的汇编程序。

我将代码更改为控制台应用程序,进行了必要的调整(抑制优化标记等),释放模式,放置断点,F5并按下ctrl-alt-d。

注意:我注意到我还必须将测试用例增加10倍才能获得这些时间。正如我预期的那样,时间完全相同。

然而,f.ex可能存在问题。注册分配,所以让我们检查一下。汇编从不撒谎。

案例1汇编程序。

                for (int i = 0; i < 500000000; i++)
00007FFE23C74526  xor         ecx,ecx  
                {
                    p.Offset(p1);
00007FFE23C74528  inc         r14d  
00007FFE23C7452B  add         r15d,2  
                for (int i = 0; i < 500000000; i++)
00007FFE23C7452F  inc         ecx  
00007FFE23C74531  cmp         ecx,1DCD6500h  
00007FFE23C74537  jl          00007FFE23C74528  
                }

案例2汇编程序

                for (int i = 0; i < 500000000; i++)
00007FFE23C84526  xor         ecx,ecx  
                {
                    p.Offset(p1);
00007FFE23C84528  inc         r14d  
00007FFE23C8452B  add         r15d,2  
                for (int i = 0; i < 500000000; i++)
00007FFE23C8452F  inc         ecx  
00007FFE23C84531  cmp         ecx,1DCD6500h  
00007FFE23C84537  jl          00007FFE23C84528  
                }

<强>结论

汇编器输出完全相同,数据也是如此 - 换句话说:内部循环的性能也完全相同。

但是,我已经解释过,请自行检查汇编器输出。如果您有另一个版本的.NET JIT,它应该解释行为。

显然,对于如何测试发出的汇编程序代码存在一些混淆。这是正确的方法:

  • 工具 - &gt;选项 - &gt;调试 - &gt;抑制模块负载的JIT优化 - &gt;取消选中(如果清除此选项,则可以调试优化的JIT代码,但调试能力可能有限)
  • 禁用&#34;只是我的代码&#34;以及相同的设置窗格。
  • 将构建类型设置为&#34; release&#34;模式。另请检查配置管理器。
  • 右键单击项目 - &gt; &#34;属性&#34; - &GT;构建标签。检查&#34;优化代码&#34;。
  • 可选,相同的标签:确保&#34;首选32位代码&#34;未经检查。 (注意:32位JIT和64位JIT是两回事。)

设置断点,F5,ctrl-alt-d,祈祷断点被击中,否则将其设置在其他地方再试一次。

我对某些评论感到有些惊讶,所以让我回顾一下:

  • @Douglas指出,较长的方法会导致代码效率降低。是的,我实际上已向Microsoft CoreCLR团队报告了此错误。这是事实,但有人说,只有在编译非常大的方法时才会禁用JIT优化器。在这种情况下非常大的是60.000行的IL代码。确切的限制可以在这里找到:https://github.com/dotnet/coreclr/blob/01a5e9b4580cf6ea21de672f627402c30658ef22/src/jit/compiler.h#L7131。在这种情况下,它完全无关紧要。
  • @Noldorin指出OP应该查看IL代码。我想指出,两种情况下发出的IL代码完全相同。