转换为IEnumerable导致性能下降

时间:2015-03-02 16:48:25

标签: c# .net performance iteration

在分析我们基于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毫秒。

3 个答案:

答案 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毫秒之间。