强制转换为IEnumerable会导致性能命中

本文关键字:性能 转换 IEnumerable | 更新日期: 2024-09-17 01:44:25

在分析我们基于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运行需要1ms。Method2需要将近700毫秒!不要试图复制这个性能命中:它不会出现在PC上的正常程序中。

不幸的是,我们可以非常可靠地在智能设备上的软件中复制它。该程序在Compact Framework 3.5、Windows窗体、Windows CE 6.0上运行。测量使用Environment.TickCount。很明显,我们的软件中一定有一个奇怪的错误会减慢枚举器的速度,我简直无法想象什么样的错误会让List类的速度减慢,只有当迭代使用List的IEnumerable接口时。

还有一个提示:在打开和关闭模式对话框(Windows窗体)后,突然两种方法都需要相同的时间:1毫秒。

强制转换为IEnumerable会导致性能命中

您需要多次运行测试,因为在一次运行中CPU可能会挂起,等等。例如,在运行method2时,您可能会移动鼠标,导致操作系统暂时允许鼠标驱动程序运行,等等。或者网络包到达,或者计时器说是时候让另一个应用程序运行了,等等,。。。换句话说,程序突然停止运行几毫秒有很多原因。

如果我运行以下程序(注意,不建议使用DateTime等):

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>的结构,并且可以inline(如果需要)。

如果使用IEnumerable<T>,编译器必须使用虚拟调用,并使用vtable查找确切的方法。这就解释了时差。尤其是因为你在循环中做的不多。换句话说,运行时必须查找实际使用的方法,因为IEnumerable<T>可以是任何东西:LinkedList<T>HashSet<T>、您自己创建的数据结构,。。。

一般规则是:类层次结构中对象的类型越高,编译器对实际实例的了解就越少,它对性能的优化就越少。

也许是第一次为IEnumerable生成模板代码?

非常感谢您的评论。

我们的开发团队正在分析这个性能错误近一周了

我们以不同的顺序和程序的不同模块运行这些测试多次具有和不具有编译器优化@CommuSoft:JIT需要更多的时间来运行代码,这是对的这是第一次。不幸的是,结果总是一样的:方法2比方法1慢大约700倍。

也许值得一提的是,在我们打开和关闭程序中的任何模式对话框之前,性能都会受到影响。什么模式的对话并不重要。性能命中将在基类Form的方法Dispose()已被调用。(Dispose方法尚未被派生类覆盖)

在分析bug的过程中,我深入到框架中,现在发现,该列表不可能是性能下降的原因。看看这段代码:

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到45ms之间。