最近我读到了Measure Early and Often for Performance, Part 2,它附带了source code和binary。
文章摘录:“我强调要可靠地创建高性能程序,您需要了解在设计过程早期使用的各个组件的性能”。
因此,我使用他的工具(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倍)。
由于此结果来自可靠来源,我想知道如何解释上述发现。
我不认为这篇文章已经过时是一个问题,因为文章本身没有说明任何关于读数的内容。它所做的只是提供一个基准测试工具。
答案 0 :(得分: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
结果:
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
这些结果表明,对一个虚拟实例的方法调用只需要稍微,比常规实例方法调用(在考虑控件之后可能需要2-3%),这只需要一点点比静态呼叫更长。这就是我所期待的。
在@colinfang评论为我的方法添加[MethodImpl(MethodImplOptions.NoInlining)]
属性后,我做了更多的游戏,我可以得出结论,微优化很复杂。以下是一些观察结果:
/optimize+
进行编译,则非虚拟实例调用实际上比控件花费的时间少了20%。如果我消除了lambda函数,并直接传递方法组:
new TimedAction("non-virtual instance", foo.DoSomething),
new TimedAction("virtual instance", foo.DoSomethingVirtual),
new TimedAction("static", Foo.DoSomethingStatic),
...然后虚拟和非虚拟呼叫最终花费的时间大约相同,但静态方法调用的时间要长得多(超过20%)。
所以是的,奇怪的东西。关键是:当您达到这种优化级别时,由于编译器,JIT甚至硬件级别的任何优化次数,都会出现意外结果。我们看到的差异可能是由于CPU的L2缓存策略无法控制的结果。这是龙。
答案 1 :(得分:0)
出现反直觉结果的原因有很多。一个原因是虚拟调用有时(可能大部分时间)发出callvirt
IL指令,以确保空检查(可能在搜索vtable时)。另一方面,如果JIT可以确定在虚拟呼叫点(以及非空引用)上只调用一个特定实现,则很可能会尝试将其转换为静态调用。 / p>
我认为这是应用程序设计中无关紧要的几件事之一。您应该考虑虚拟/密封语言构造,而不是运行时(让运行时尽其所能)。如果某个方法需要为您的应用程序需求提供虚拟,请将其设置为虚拟。如果它不需要是虚拟的,请不要成功。如果你确实不将基于你的应用程序的设计作为基础,那么就没有必要对它进行基准测试。 (除了好奇心。)