在分析我们基于Windows CE的软件的性能影响时,我偶然发现了一个问题 迷人的神秘:
看看这两种方法:
void Method1(List<int> list)
{
foreach (var item in list)
{
if (item == 2000)
break;
}
}
void Method2(List<int> list)
{
foreach (var item in (IEnumerable<int>)list)
{
if (item == 2000)
break;
}
}
void StartTest()
{
var list = new List<int>();
for (var i = 0; i < 3000; i++)
list.Add(i);
StartMeasurement();
Method1(list);
StopMeasurement(); // 1 ms
StartMeasurement();
Method2(list);
StopMeasurement(); // 721 ms
}
void StartMeasurement()
{
_currentStartTime = Environment.TickCount;
}
void StopMeasurement()
{
var time = Environment.TickCount - _currentStartTime;
Debug.WriteLine(time);
}
Method1
需要1毫秒才能运行。 Method2
需要近700毫秒!
不要尝试重现这种性能影响:它不会出现在PC上的正常程序中。
不幸的是,我们可以非常可靠地在我们的智能设备上的软件中重现它。该程序在Compact Framework 3.5,Windows Forms,Windows CE 6.0上运行。
测量使用Environment.TickCount
。
很明显,我们的软件中一定存在一个奇怪的错误,它会减慢枚举器的速度,我只是无法想象什么样的错误可以让List
类减速,只有迭代是使用List的IEnumerable
接口。
还有一个提示:在打开和关闭模式对话框(Windows窗体)之后,突然两种方法都需要相同的时间:1毫秒。
答案 0 :(得分:3)
您需要多次运行测试,因为在一次运行中, CPU 可能会被暂停,等等。例如,在运行method2
时,您可能正在使用鼠标导致操作系统临时让鼠标驱动程序运行等。或者网络包到达,或者计时器说它是时候让另一个应用程序运行,...换句话说,有很多原因导致突然间你的程序停止运行几毫秒。
如果我运行以下程序(请注意,使用DateTime
&#39;等)不建议:
var list = new List<int>();
for (var i = 0; i < 3000; i++)
list.Add(i);
DateTime t0 = DateTime.Now;
for(int i = 0; i < 50000; i++) {
Method1(list);
}
DateTime t1 = DateTime.Now;
for(int i = 0; i < 50000; i++) {
Method2(list);
}
DateTime t2 = DateTime.Now;
Console.WriteLine(t1-t0);
Console.WriteLine(t2-t1);
我明白了:
00:00:00.6522770 (method1)
00:00:01.2461630 (method2)
交换测试结果的顺序:
00:00:01.1278890 (method2)
00:00:00.5473190 (method1)
所以它的速度只有100%。此外,第一种方法(method1
)的性能可能稍好一些,因为对于method1
,JIT编译器首先需要将代码转换为机器指令。换句话说,第一种方法调用你往往比过程中稍晚的那些慢。。
延迟可能是因为如果你使用List<T>
,编译器可以专门化 foreach
循环:已经知道结构在编译时IEnumerator<T>
,如果有必要,可以内联。
如果使用IEnumerable<T>
,编译器必须使用虚拟调用并使用 vtable 查找确切的方法。这解释了时差。特别是因为你在你的循环中做不了多少。换句话说,运行时必须查找实际使用的方法,因为IEnumerable<T>
可以是任何内容:LinkedList<T>
,HashSet<T>
,您自己制作的数据结构,......
一般规则是:类层次结构中对象的类型越高,编译器对实际实例的了解越少,优化性能的能力就越小。
答案 1 :(得分:0)
也许第一次为IEnumerable生成模板代码?
答案 2 :(得分:0)
非常感谢你的所有评论。
我们的开发团队正在分析这个性能错误近一周
我们以不同的顺序和程序的不同模块多次运行这些测试 有和没有编译器优化。 @CommuSoft:你是对的,JIT需要更多的时间来运行代码 首次。不幸的是结果总是一样的: Method2比Method1慢大约700倍。
也许它值得再次提及,性能命中似乎直到我们打开并关闭程序中的任何模态对话框。什么模态对话并不重要。性能打击将在之后立即修复 方法已调用基类Form的Dispose()。 (Dispose方法还没有 由派生类覆盖)
在我分析这个错误的过程中,我深入了解框架,现在发现了 列表不能成为性能影响的原因。看看这段代码:
class Test
{
void Test()
{
var myClass = new MyClass();
StartMeasurement();
for (int i = 0; i < 5000; i++)
{
myClass.DoSth();
}
StopMeasurement(); // ==> 46 ms
var a = (IMyInterface)myClass;
StartMeasurement();
for (int i = 0; i < 5000; i++)
{
a.DoSth();
}
StopMeasurement(); // ==> 665 ms
}
}
public interface IMyInterface
{
void DoSth();
}
public class MyClass : IMyInterface
{
public void DoSth()
{
for (int i = 0; i < 10; i++ )
{
double a = 1.2345;
a = a / 19.44;
}
}
}
通过接口调用方法比直接调用它需要更多的时间。 但当然在关闭我们可疑的对话框后,我们测量了两个循环的时间在44到45毫秒之间。