当唯一的区别在于未执行的代码路径时,为什么性能会有所不同?

时间:2015-07-09 05:59:32

标签: .net performance

下面的

Test1始终比Test2快10%,尽管我总是使用0参数调用该方法,所以内部的内容是切换案例 - 唯一的区别 - 永远不会被执行。

将代码复制并粘贴到一个全新的项目后,只将测试功能的名称更改为Main,结果就会反转。每次我运行该项目时,Test2都会更快。

那么让这个更慢更快的因素是什么? 并且:我可以故意在.net中影响性能吗?

这些方法当然几乎没有做任何事情,因此对于主要涉及相同虚拟方法调用的测试(其中,CLR无法内联)的性能差异为10% - 对于在BVM中实现这一点而言,请参阅Sun btw )似乎很大。

N.B。这实际上是一个真实程序的最小版本,其中多个嵌套的switch语句导致巨大的性能差异不仅仅是10%,而是100%甚至更多,显然是由于嵌套交换机分支内部的代码存在而导致测试永远不要进入(因此,这个最小版本可能会忽略可能涉及的实际程序的其他一些方面,但它确实会复制显着且一致的性能差异)

编辑在真实的程序中,可能比这个问题中的现象更重要的是,switch语句中的实际case语句是否适合通过branch table实现 - (这取决于案例值中的差距 - 我可以通过查看生成的IL代码来验证这一点)

试运行

  • .net 4.5.1
  • 发布版本,通过Ctrl-F5
  • 运行
  • Intel i7 CPU

代码:

using System;
using System.Diagnostics;

class Test1 : ITest
{
    public int Test(int a)
    {
        switch (a)
        {
            case 1: return a + a + a == 1234 ? 1 : 2;
            case 2: return 2;
        }
        return 0;
    }
}

class Test2 : ITest
{
    public int Test(int a)
    {
        switch (a)
        {
            case 1: return 1;
            case 2: return 2;
        }
        return 0;
    }
}

class Program
{
    static void Main(string[] args)
    {
        const long iterations = 200000000;
        var test1 = new Test1();
        var test2 = new Test2();

        while (true)
        {
            var sw1 = Stopwatch.StartNew();
            for (long i = 0; i < iterations; i++)
                test1.Test(0);
            sw1.Stop();


            var sw2 = Stopwatch.StartNew();
            for (long i = 0; i < iterations; i++)
                test2.Test(0);
            sw2.Stop();

            var iterPerUsec1 = iterations / sw1.Elapsed.TotalMilliseconds / 1000;
            var iterPerUsec2 = iterations / sw2.Elapsed.TotalMilliseconds / 1000;
            Console.WriteLine("iterations per usec: " + (int) iterPerUsec1 + " / " + (int) iterPerUsec2 + " ratio: " + iterPerUsec1/iterPerUsec2);
        }
    }
}

interface ITest
{
    int Test(int a);
}

这是典型运行的输出,其中Test1的速度实际上持续超过12%:

iterations per usec: 369 / 342 ratio: 1.07656329512607
iterations per usec: 367 / 314 ratio: 1.16820632522335
iterations per usec: 372 / 337 ratio: 1.10255744679504
iterations per usec: 374 / 342 ratio: 1.09248387354978
iterations per usec: 367 / 329 ratio: 1.11451205881061
iterations per usec: 375 / 340 ratio: 1.10041698470293
iterations per usec: 373 / 314 ratio: 1.19033461920118
iterations per usec: 366 / 334 ratio: 1.09808424282708
iterations per usec: 372 / 314 ratio: 1.18497411681768
iterations per usec: 377 / 342 ratio: 1.10482425370152
iterations per usec: 380 / 346 ratio: 1.09794853154766
iterations per usec: 385 / 342 ratio: 1.12737583603649
iterations per usec: 376 / 327 ratio: 1.15024393718844
iterations per usec: 374 / 332 ratio: 1.12400483908544
iterations per usec: 383 / 341 ratio: 1.12106159857722
iterations per usec: 380 / 345 ratio: 1.10267634674555
iterations per usec: 375 / 344 ratio: 1.09211401775982
iterations per usec: 384 / 334 ratio: 1.14958454236246
iterations per usec: 368 / 321 ratio: 1.14575850263002
iterations per usec: 378 / 335 ratio: 1.12732301818235
iterations per usec: 380 / 338 ratio: 1.12375853123099
iterations per usec: 386 / 344 ratio: 1.12213818994067
iterations per usec: 385 / 336 ratio: 1.14346447712043
iterations per usec: 374 / 345 ratio: 1.08448615249764
...

2 个答案:

答案 0 :(得分:10)

基准测试是一门艺术,很难可靠地测量这样快速的代码。通常,15%或更小的差异不是统计学上显着的结果。我只能评论发布的代码中的缺陷,这是一个非常常见的。典型的海森堡人,它是影响结果的测试本身。

第二个for()循环不像第一个for()循环那样优化。优化程序选择将哪些局部变量存储在CPU寄存器中引起的问题。特别是在32位程序中使用 long 时会出现问题,它会烧掉两个CPU寄存器。很可能,提到无法重现它的评论者使用x64抖动进行了测试。

通过将测试移到单独的方法中来减轻CPU寄存器分配器的压力:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.dialog_nearby);

        WebView wv;  
        wv = (WebView) findViewById(R.id.WebView);  
        wv.loadUrl("file:///android_asset/intro.html"); 
    }

现在你会得到你所期望的。

通过查看生成的机器代码来提示此更改。工具&gt;选项&gt;调试&gt;一般&gt;取消选中Suppress JIT Optimization选项。然后,您可以使用Debug&gt;查看优化的机器代码。 Windows&gt;拆卸。其中显示了第一个for()循环:

<WebView 
            android:id="@+id/WebView"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"             
            />

第二个循环:

static Stopwatch runTest1(Test1 test1, long iterations) {
    var sw1 = Stopwatch.StartNew();
    for (long i = 0; i < iterations; i++)
        test1.Test(0);
    sw1.Stop();
    return sw1;
}

static Stopwatch runTest2(Test2 test2, long iterations) {
    var sw2 = Stopwatch.StartNew();
    for (long i = 0; i < iterations; i++)
        test2.Test(0);
    sw2.Stop();
    return sw2;
}

static void Main(string[] args) {
    const long iterations = 200000000;
    var test1 = new Test1();
    var test2 = new Test2();

    while (true) {
        var sw1 = runTest1(test1, iterations);
        var sw2 = runTest2(test2, iterations);
        // etc..
    }
}

我标记了使它变慢的指令。第一个for()循环可以将循环变量存储在esi:ebx寄存器中并将它们保存在那里。这在第二个for()循环中不起作用,它耗尽了可用的CPU寄存器,并且循环变量的前32位必须存储在堆栈帧中。那很慢。

在这样的程序中进行更改不是通用建议,也不是通用解决方案。它恰好在这个特定的案例中有效。只查看机器代码可以为您提供这样的更改可能有用的提示。这是关于手动调整代码所需要的。您实际上尝试优化的内容可能更多,此基准测试不太可能代表您的实际代码。

答案 1 :(得分:2)

为了调查我接受了你的代码并略微修改了它。我的主循环迭代20次,我已经删除了打印两次测试速度的部分。相反,我将差异存储在预分配数组中的刻度(void translate(std::string &str, const std::string &from, const std:string &to) { std::size_t at = 0; for (;;) { at = str.find(from, at); if (at == str.npos) break; str.replace(at, from.size(), to); } } std::string lineEnd = getFromDatabase(); translate(lineEnd, "\\r", "\r"); translate(lineEnd, "\\n", "\n"); )中。这只是为了使主循环尽可能简单。

在主循环结束时,我平均了滴答差异。正数表示第二次测试使用比第一次测试更多的刻度执行得更慢。

我使用可能的测试序列的所有排列进行了此测试:sw2.ElapsedTicks - sw1.ElapsedTicks后跟Test1Test2后跟Test2Test1后跟{{ 1}}和Test1后跟Test1。这些是我开启优化的结果:

 Tests | Average
-------+---------
 1 2   | 248,747
 2 1   | 313,372
 1 1   | 234.812
 2 2   | 210.533

所以无论我如何运行测试,循环中的第二个测试总是慢于第一个。

我的结论是,由于某种原因,主循环中的第一次测试执行得会稍快一些。我不知道为什么,我并不总是得到一致的结果,但至少这与Test2Test2中的代码无关。