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

Eug*_*sky 13 .net performance

Test1Test2虽然我总是用一个0参数调用方法,但是下面的内容始终快10%,所以切换案例中的内容 - 唯一的区别 - 永远不会被执行.

在将代码复制并粘贴到一个全新的项目中之后,仅将测试函数的名称更改为Main,结果是相反的.每次我运行该项目,Test2都会更快.

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

这些方法当然几乎没有做任何事情,因此对于主要涉及相同虚拟方法调用的测试(其中,CLR无法内联 - 对于Sun在bVM中实现它的开头语来说)的性能差异似乎很大.

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

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

测试运行

  • .net 4.5.1
  • 发布版本,通过Ctrl-F5运行
  • 英特尔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);
}
Run Code Online (Sandbox Code Playgroud)

以下是典型运行的输出,其中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
...
Run Code Online (Sandbox Code Playgroud)

Han*_*ant 10

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

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

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

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..
    }
}
Run Code Online (Sandbox Code Playgroud)

而且你现在可以得到你所期望的.

通过查看生成的机器代码来确定此更改.工具>选项>调试>常规>取消勾选抑制JIT优化选项.然后,您可以使用Debug> Windows> Disassembly查看优化的机器代码.这显示了第一个for()循环:

            for (long i = 0; i < iterations; i++)
00000089  xor         ebx,ebx 
0000008b  xor         esi,esi 
0000008d  mov         ecx,dword ptr [esp+4] 
00000091  xor         edx,edx 
00000093  call        dword ptr ds:[04732844h] 
00000099  add         ebx,1 
0000009c  adc         esi,0 
0000009f  test        esi,esi 
000000a1  jg          000000AD 
000000a3  jl          0000008D 
000000a5  cmp         ebx,0BEBC200h 
000000ab  jb          0000008D 
Run Code Online (Sandbox Code Playgroud)

第二个循环:

            for (long i = 0; i < iterations; i++)
000000f5  mov         dword ptr [esp+2Ch],0 
000000fd  xor         ebx,ebx 
000000ff  mov         ecx,dword ptr [esp] 
00000102  xor         edx,edx 
00000104  call        dword ptr ds:[056E28B8h] 
0000010a  mov         eax,ebx 
0000010c  mov         edx,dword ptr [esp+2Ch]        ; <== here
00000110  add         eax,1 
00000113  adc         edx,0 
00000116  mov         dword ptr [esp+2Ch],edx        ; <== here
0000011a  mov         ebx,eax 
0000011c  cmp         dword ptr [esp+2Ch],0 
00000121  jg          0000012D 
00000123  jl          000000FF 
00000125  cmp         ebx,0BEBC200h 
0000012b  jb          000000FF 
Run Code Online (Sandbox Code Playgroud)

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

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