虚方法比非虚方法快?

col*_*ang 4 c# performance

最近我读到了关于早期测量和经常性能的测试,第2部分,它附带了源代码二进制文件.

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

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

在我的PC(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
Run Code Online (Sandbox Code Playgroud)

我很惊讶地发现虚方法(0.674)比非虚拟实例方法(1.009)或静态方法(1)快.而界面根本不是太慢!(我希望界面速度至少为2倍).

由于这个结果来自可信赖的来源,我想知道如何解释上述发现.

我不认为这篇文章已经过时是一个问题,因为文章本身并没有说明任何关于读数的内容.它所做的只是提供一个基准测试工具.

Str*_*ior 5

我猜他的例子中使用的基准测试方法是有缺陷的.以下代码在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
Run Code Online (Sandbox Code Playgroud)

结果:

                       DryRun1 DryRun2  FullRun1 FullRun2
 control               0.0361  0        47.82    47.1971 
 non-virtual instance  0.0858  0.0004   69.6178  68.7508 
 virtual instance      0.1676  0.0004   70.5103  69.2135 
 static                0.1138  0        66.6182  67.0308 
Run Code Online (Sandbox Code Playgroud)

结论

这些结果表明,一个方法调用到一个虚拟实例只需要稍微长(可能由2-3%,占控制后)比普通的实例方法调用,它不是一个静态调用只需要稍微长一点.这就是我所期待的.

更新

在@colinfang评论[MethodImpl(MethodImplOptions.NoInlining)]为我的方法添加属性之后,我做了更多的游戏,我可以得出结论,微优化很复杂.以下是一些观察结果:

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

    new TimedAction("non-virtual instance", foo.DoSomething),
    new TimedAction("virtual instance", foo.DoSomethingVirtual),
    new TimedAction("static", Foo.DoSomethingStatic),
    
    Run Code Online (Sandbox Code Playgroud)

    ...然后虚拟和非虚拟呼叫最终花费的时间大约相同,但静态方法调用需要更长时间(超过20%).

所以,是的,奇怪的东西.关键是:当您达到这种优化级别时,由于编译器,JIT甚至硬件级别的任何优化次数,都会出现意外结果.我们看到的差异可能是由于CPU的L2缓存策略无法控制的结果.这里是龙.