gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-03 23:47:39 25 4
gpt4 key购买 nike

Test1以下始终比 Test2 快 10% ,尽管我总是用 0 调用该方法参数,所以 switch case 里面的东西——唯一的区别——永远不会被执行。

顺便说一句,将代码复制并粘贴到一个全新的项目中后,只需将测试函数的名称更改为 Main ,结果反过来。每次我运行那个项目时,Test2是比较快的。

那么是什么因素导致这个速度越来越慢呢?
和:我可以故意影响 .net 中的性能以对我有利吗?

这些方法当然几乎什么都不做,因此对于主要涉及相同虚拟方法调用的测试而言,10% 的性能差异似乎很大.

注意这实际上是真实程序的最小版本,其中多个嵌套的 switch 语句会导致巨大的性能差异,不仅是 10%,而且是 100% 甚至更多,这显然是因为测试的嵌套 switch 分支内仅存在代码永远不要进入。 (所以也许这个最小版本省略了可能涉及的真实程序的一些其他方面,但它确实复制了显着且一致的性能差异)

编辑 在实际程序中,可能比这个问题中的现象更重要的是 switch 语句中的实际 case 语句是否适合通过 branch table 实现。与否 - (这取决于案例值中的差距 - 我可以通过查看生成的 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);
    }

    这是典型运行的输出,其中 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
    ...

    最佳答案

    基准测试是一门艺术,像这样可靠地测量非常快的代码是非常困难的。一般来说,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..
    }
    }

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

    这种变化是通过查看生成的机器代码来提示的。工具 > 选项 > 调试 > 常规 > 取消勾选抑制 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

    第二个循环:
                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

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

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

    关于.net - 当唯一的区别在于未执行的代码路径时,为什么性能会有所不同?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31309289/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com