【问题标题】:Why subsequent direct method call is much faster than the first call?为什么后续的直接方法调用比第一次调用快得多?
【发布时间】:2019-08-24 12:27:13
【问题描述】:

正如this MSDN article 中所解释的,当使用诸如InvokeMember 之类的.NET 反射API 时,由于元数据的缓存,第一次调用比后续调用需要更多的时间来运行。

当我在不使用反射的情况下测试直接方法调用时,我在 Mono 和 .NET 4 上也看到了类似的效果。

第一个数字是操作的结果,'-'后面的第二个数字是这个操作花费的时间,单位是毫秒。我使用 '

300 - 0.192 <--
300 - 0.004
300 - 0.004
-100 - 0.096 <--
-100 - 0.004
-100 - 0.004

这是为什么?我可以理解第一次调用可能会慢一些,但慢 50 倍并不是我所期望的。

附上得到这个结果的源代码。

图书馆

namespace MyClass
{
    public class Calculator
    {
        public int Value1 {get; set;}
        public int Value2 {get; set;}
        public Calculator()
        {
            Value1 = 100;
            Value2 = 200;
        }

        public int Add(int val1, int val2)
        {
            Value1 = val1; Value2 = val2;
            return Value1 + Value2;
        }

        public int Sub(int val1, int val2)
        {
            Value1 = val1; Value2 = val2;
            return Value1 - Value2;
        }
    }
}

调用这个库的代码

// http://msdn.microsoft.com/en-us/magazine/cc163759.aspx
using System;
using System.IO;
using System.Reflection;
using System.Diagnostics;
using System.Collections.Generic;
using MyClass;

class TestOne
{
    static void DirectTest()
    {
        Stopwatch sw;
        Calculator t = new Calculator();

        sw = Stopwatch.StartNew();
        int value1 = t.Add(100,200);
        sw.Stop();
        double time1 = sw.Elapsed.TotalMilliseconds;

        sw = Stopwatch.StartNew();
        int value2 = t.Add(100,200);   
        sw.Stop();
        double time2 = sw.Elapsed.TotalMilliseconds;

        sw = Stopwatch.StartNew();
        int value3 = t.Add(100,200); 
        sw.Stop();
        double time3 = sw.Elapsed.TotalMilliseconds;

        Console.WriteLine("{0} - {1}", value1, time1);
        Console.WriteLine("{0} - {1}", value2, time2);
        Console.WriteLine("{0} - {1}", value3, time3);

        sw = Stopwatch.StartNew();
        value1 = t.Sub(100,200);
        sw.Stop();
        time1 = sw.Elapsed.TotalMilliseconds;

        sw = Stopwatch.StartNew();
        value2 = t.Sub(100,200);  
        sw.Stop();
        time2 = sw.Elapsed.TotalMilliseconds;

        sw = Stopwatch.StartNew();
        value3 =  t.Sub(100,200); 
        sw.Stop();
        time3 = sw.Elapsed.TotalMilliseconds;

        Console.WriteLine("{0} - {1}", value1, time1);
        Console.WriteLine("{0} - {1}", value2, time2);
        Console.WriteLine("{0} - {1}", value3, time3);
    }
    static void Main()
    {
        DirectTest();
        DirectTest();
    }
}

【问题讨论】:

  • 不要认为第一次调用会慢 50 倍,而认为后续调用会快 50 倍
  • @Travis Gockel:没错,但在某些情况下,您真正​​关心的是第一次通过。当然有办法解决这个问题。
  • 可能是任何情况,很可能是外部 CPU 压力。这只是一个(非常)糟糕的基准。
  • @Henk Holterman:不,不可能是什么,肯定是JIT编译通。

标签: c# .net performance


【解决方案1】:

这是因为用于 .NET 应用程序的 Just In Time (JIT) 编译方法。 MSIL bytecode 由 JIT 编译器一次转换为机器代码,并且该代码的后续执行速度要快得多,因为已经生成并缓存了本机版本。

您在运行代码时需要支付一次性费用,但 JIT 编译器还可以针对当前架构执行优化,如果代码从一开始就是本机代码则无法执行。但是,您可以通过调用 RuntimeHelpers.PrepareMethod.

来强制通过 JIT

【讨论】:

  • 可能但不太可能是全部答案。通过在第一个 Stopwatch 之前立即调用方法来轻松测试。
  • @Henk Holterman:“不可能是全部答案。” - 为什么?你还有什么建议?这看起来与我的 JIT 通行证一模一样。您还有其他建议吗?
  • @Henk Holterman:嗯,我可以看到他的代码,所以我们知道那里没有线程问题。当然,Windows 不是 RTOS,但这些数字不会说谎并且非常可重复。除非机器完全受 CPU 限制,否则调度问题不会造成如此大的差异,在这种情况下,我们将看不到如此可重复的数字。我已经花费了相当多的时间来解决 .NET 应用程序中的 CPU 瓶颈问题,但我在这里看不到任何东西表明这只是第一次 JIT 传递的结果。
  • 您是否也将秒表放在对 2 行方法的单次调用中?
  • @Henk Holterman:那会有什么不同?如果有的话,代码将执行得如此之快以至于在秒表的分辨率下,但考虑到.194ms 结果,这里的情况并非如此。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多