C# 虚拟方法比非虚拟方法快?

C# 虚拟方法比非虚拟方法快?,c#,performance,C#,Performance,最近我读到关于的,它附带了和 文章摘录:“我强调,要可靠地创建高性能程序,您需要了解在设计过程早期使用的各个组件的性能” 因此,我使用他的工具(v0.2.2)进行基准测试,并尝试查看各个组件的性能 在我的电脑(x64)下,结果如下: Name Median Mean StdDev Min Max Samples NOTHING [

最近我读到关于的,它附带了和

文章摘录:“我强调,要可靠地创建高性能程序,您需要了解在设计过程早期使用的各个组件的性能”

因此,我使用他的工具(v0.2.2)进行基准测试,并尝试查看各个组件的性能

在我的电脑(x64)下,结果如下:

Name                                                                            Median  Mean    StdDev  Min     Max Samples
NOTHING [count=1000]                                                            0.14    0.177   0.164   0       0.651   10
MethodCalls: EmptyStaticFunction() [count=1000 scale=10.0]                      1       1.005   0.017   0.991   1.042   10
Loop 1K times [count=1000]                                                      85.116  85.312  0.392   84.93   86.279  10
MethodCalls: EmptyStaticFunction(arg1,...arg5) [count=1000 scale=10.0]          1.163   1.172   0.015   1.163   1.214   10
MethodCalls: aClass.EmptyInstanceFunction() [count=1000 scale=10.0]             1.009   1.011   0.019   0.995   1.047   10
MethodCalls: aClass.Interface() [count=1000 scale=10.0]                         1.112   1.121   0.038   1.098   1.233   10
MethodCalls: aSealedClass.Interface() (inlined) [count=1000 scale=10.0]         0       0.008   0.025   0       0.084   10
MethodCalls: aStructWithInterface.Interface() (inlined) [count=1000 scale=10.0] 0       0.008   0.025   0       0.084   10
MethodCalls: aClass.VirtualMethod() [count=1000 scale=10.0]                     0.674   0.683   0.025   0.674   0.758   10
MethodCalls: Class.ReturnsValueType() [count=1000 scale=10.0]                   2.165   2.16    0.033   2.107   2.209   10
我惊讶地发现虚拟方法(0.674)比非虚拟实例方法(1.009)或静态方法(1)更快。而且界面一点也不慢!(我希望接口的速度至少是原来的2倍)

由于这些结果来自可靠的来源,我想知道如何解释上述发现


我不认为这篇文章过时是一个问题,因为在文章本身,它没有说任何关于阅读。它所做的只是提供了一个基准测试工具。

我想他的例子中使用的基准测试方法是有缺陷的。以下在LINQPad中运行的代码显示了您的预期:

/* This is a benchmarking template I use in LINQPad when I want to do a
 * quick performance test. Just give it a couple of actions to test and
 * it will give you a pretty good idea of how long they take compared
 * to one another. It's not perfect: You can expect a 3% error margin
 * under ideal circumstances. But if you're not going to improve
 * performance by more than 3%, you probably don't care anyway.*/
void Main()
{
    // Enter setup code here
    var foo = new Foo();
    var actions = new[]
    {
        new TimedAction("control", () =>
        {
            // do nothing
        }),
        new TimedAction("non-virtual instance", () =>
        {
            foo.DoSomething();
        }),
        new TimedAction("virtual instance", () =>
        {
            foo.DoSomethingVirtual();
        }),
        new TimedAction("static", () =>
        {
            Foo.DoSomethingStatic();
        }),
    };
    const int TimesToRun = 10000000; // Tweak this as necessary
    TimeActions(TimesToRun, actions);
}

public class Foo
{
    public void DoSomething() {}
    public virtual void DoSomethingVirtual() {}
    public static void DoSomethingStatic() {}
}


#region timer helper methods
// Define other methods and classes here
public void TimeActions(int iterations, params TimedAction[] actions)
{
    Stopwatch s = new Stopwatch();
    int length = actions.Length;
    var results = new ActionResult[actions.Length];
    // Perform the actions in their initial order.
    for(int i = 0; i < length; i++)
    {
        var action = actions[i];
        var result = results[i] = new ActionResult{Message = action.Message};
        // Do a dry run to get things ramped up/cached
        result.DryRun1 = s.Time(action.Action, 10);
        result.FullRun1 = s.Time(action.Action, iterations);
    }
    // Perform the actions in reverse order.
    for(int i = length - 1; i >= 0; i--)
    {
        var action = actions[i];
        var result = results[i];
        // Do a dry run to get things ramped up/cached
        result.DryRun2 = s.Time(action.Action, 10);
        result.FullRun2 = s.Time(action.Action, iterations);
    }
    results.Dump();
}

public class ActionResult
{
    public string Message {get;set;}
    public double DryRun1 {get;set;}
    public double DryRun2 {get;set;}
    public double FullRun1 {get;set;}
    public double FullRun2 {get;set;}
}

public class TimedAction
{
    public TimedAction(string message, Action action)
    {
        Message = message;
        Action = action;
    }
    public string Message {get;private set;}
    public Action Action {get;private set;}
}

public static class StopwatchExtensions
{
    public static double Time(this Stopwatch sw, Action action, int iterations)
    {
        sw.Restart();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();

        return sw.Elapsed.TotalMilliseconds;
    }
}
#endregion
结论 这些结果表明,对虚拟实例的方法调用只需要比常规实例方法调用稍长的时间(考虑控件后,可能需要2-3%),而常规实例方法调用只需要比静态调用稍长的时间。这正是我所期望的

更新 @colinfang评论说在我的方法中添加了
[MethodImpl(MethodImplOptions.noinline)]
属性后,我做了更多的尝试,我能得出的结论是微观优化是复杂的。以下是一些观察结果:

  • 正如@colinfang所说,在方法中添加noinline确实会产生更像他所描述的结果。毫不奇怪,方法内联是系统优化非虚拟方法以比虚拟方法更快的一种方法。但令人惊讶的是,不内联实际上会使虚拟方法比非虚拟方法花费更长的时间
  • 如果我使用
    /optimize+/
    进行编译,那么非虚拟实例调用实际上比控件花费的时间少20%以上
  • 如果我消除lambda函数,并像这样直接传递方法组:

    new TimedAction("non-virtual instance", foo.DoSomething),
    new TimedAction("virtual instance", foo.DoSomethingVirtual),
    new TimedAction("static", Foo.DoSomethingStatic),
    
    。。。然后,虚拟调用和非虚拟调用最终花费的时间大致相同,但静态方法调用花费的时间要长得多(高达20%)


是啊,奇怪的东西。关键是:当您进入这个优化级别时,由于编译器、JIT甚至硬件级别的任何数量的优化,都会出现意外的结果。我们看到的差异可能是CPU的二级缓存策略无法控制的结果。这里是龙。

为什么会出现反直觉的结果有很多原因。一个原因是虚拟调用有时(可能大部分时间)会发出
callvirt
IL指令,以确保空检查(可能是在搜索vtable时)。另一方面,如果JIT能够确定在虚拟调用点(并且在非空引用上)只调用一个特定的实现,那么它很可能会尝试将其转换为静态调用


我认为这是在应用程序设计中真正不重要的少数事情之一。您应该考虑虚拟/密封的语言构造,而不是运行时构造(让运行时尽其所能)。如果一个方法需要是虚拟的,以满足应用程序的需要,请将其设置为虚拟的。如果它不需要是虚拟的,就不要做。如果您确实不打算将应用程序的设计基于此,那么就没有必要对其进行基准测试。(好奇除外。)

接口调用是一个虚拟调用。您链接的文章几乎肯定是要衡量您自己代码的性能,而不是调用它的机制。几乎可以肯定的是,在虚拟电话和非虚拟电话的功能之间做出决定的能力将比性能上的任何微小差异都要重要得多。在这篇博客文章中,有一点值得注意:那就是2008年5月。5年内可能会有很多变化。我根本不同意“尽早并经常衡量绩效”的原则。在我看来,正确性应该高于所有其他品质。“优化是最后一件事。”挥霍者说,这取决于你在做什么。我认为博客更多的是为了图书馆作者而不是应用程序开发人员。使用库代码时可能会有非常不同的思维方式。您是否介意将
[MethodImpl(methodimpoptions.noinline)]
添加到
Foo
下的所有方法中?添加它之后,我能够重现与我的帖子中类似的结果。C#编译器对几乎所有实例方法的调用都使用
callvirt
,无论目标方法是否为虚拟方法。我知道的一个例外是,当您使用
base.Foo(…)
显式调用基类实现时。@280Z28,有趣的是,它似乎不被规范允许:规范甚至要求对
base.Foo(…)
调用进行空检查(只有在直接或间接地从C#以外的其他语言调用时,才能使用
null
调用此
)。我很确定这是规范中的错误,而不是编译器中的错误。@280Z28我不知道。谢谢。@hvd
base.Foo(…)
需要直接分派到特定的方法,甚至(特别是)如果该方法已被重写。如果它使用
callvirt
,则最终该方法会反复调用自身。@280Z28规范没有说明任何关于
call
/
callvirt
,规范说对
null
的方法调用会导致
NullReferenceException
。没有相关的异常。我知道
cal>l
不会导致
NullReferenceException
,但这仅仅意味着
调用
是不够的,一个合格的C#编译器应该是emi
new TimedAction("non-virtual instance", foo.DoSomething),
new TimedAction("virtual instance", foo.DoSomethingVirtual),
new TimedAction("static", Foo.DoSomethingStatic),