代码改变世界

泛型真的会降低性能吗?

2009-05-29 16:41  Jeffrey Zhao  阅读(...)  评论(... 编辑 收藏

在《.NET,你忘记了么?(八)—— 从dynamic到特性误用》一文中,飞林沙同学提到,使用泛型会略微降低程序性能,因此在程序中使用List<Object>是不合理的行为,应该使用ArrayList。这一点和老赵平时的观点相悖,老赵一直提倡,在.NET 2.0之后,要尽可能使用List<T>,情愿是List<Object>也不要使用ArrayList。不过个中原因与性能无关,我们稍候再叙述。飞同学的文章让我有了将泛型与非泛型进行性能比较的想法。这个比较非常容易,不过也得出了一些非常有意思的结论。

泛型容器与非泛型容器的性能比较

首先,我们来比较一种最“纯粹”的泛型容器,它的目的是避免程序的其他方面对性能的影响。因此,这里我们构造两个最简单的容器,就是简单的模仿ArrayList和List<T>:

public class MyArrayList
{
    public MyArrayList(int length)
    {
        this.m_items = new object[length];
    }

    public object[] m_items;

    public object this[int index]
    {
        get
        {
            return this.m_items[index];
        }
        set
        {
            this.m_items[index] = value;
        }
    }

    public IEnumerable InnerContainer
    {
        get
        {
            return this.m_items;
        }
    }
}

public class MyList<T>
{
    public MyList(int length)
    {
        this.m_items = new T[length];
    }

    public T[] m_items;

    public T this[int index]
    {
        get
        {
            return this.m_items[index];
        }
        set
        {
            this.m_items[index] = value;
        }
    }

    public IEnumerable<T> InnerContainer
    {
        get
        {
            return this.m_items;
        }
    }
}

MyArrayList为直接使用Object类型的容器,而MyList<T>则是泛型容器。老赵为他们实现了两种操作,一是下标访问,二是直接把内部的数组容器作为可遍历的对象释放出来。这两种都是平时编程中最经常使用的操作。

于是我们就可以编写测试代码了。首先,我们初始化两种容器,并进行“预热”:

int length = 1000;
object value = new object();

MyArrayList myArrayList = new MyArrayList(length);
for (int i = 0; i < length; i++)
{
    myArrayList[i] = myArrayList[i] ?? value;
}

MyList<object> myList = new MyList<object>(length);
for (int i = 0; i < length; i++)
{
    myList[i] = myList[i] ?? value;
}

然后我们使用CodeTimer来进行统计:

CodeTimer.Initialize();
int iteration = 300 * 1000;

CodeTimer.Time("MyArrayList下标访问", iteration, () =>
{
    for (int i = 0; i < length; i++)
    {
        var o = myArrayList[i];
    }
});

CodeTimer.Time("MyList下标访问", iteration, () =>
{
    for (int i = 0; i < length; i++)
    {
        var o = myList[i];
    }
});

Release Build并运行。猜猜看,结果是什么?

MyArrayList下标访问
        Time Elapsed:   2,398ms
        CPU Cycles:     5,042,561,997
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0

MyList下标访问
        Time Elapsed:   2,285ms
        CPU Cycles:     4,935,066,741
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0

以上是在老赵的机器上得到的结果,从结果上看,泛型的MyList性能甚至略比MyArrayList有所提高。当然测试的结果其实是互有胜负,但是事实上,MyList的获胜的次数甚至还略有领先。

那么我们再来看看“遍历”的性能如何:

CodeTimer.Time("MyArrayList遍历", iteration, () =>
{
    foreach (object o in myArrayList.InnerContainer)
    {
        var o1 = o;
    }
});

CodeTimer.Time("MyList遍历", iteration, () =>
{
    foreach (object o in myList.InnerContainer)
    {
        var o1 = o;
    }
});

运行的结果颇有意思:

MyArrayList遍历
        Time Elapsed:   21,367ms
        CPU Cycles:     46,023,627,496
        Gen 0:          2
        Gen 1:          1
        Gen 2:          0

MyList遍历
        Time Elapsed:   3,463ms
        CPU Cycles:     7,448,928,223
        Gen 0:          2
        Gen 1:          0
        Gen 2:          0

直接使用Object的MyArrayList性能居然差了这么多!个中原因老赵有所猜测,在得到明确答案之后会接着与大家分享。

ArrayList和List<T>的性能

刚才比较了最“纯粹”的性能,那么我们再来比较ArrayList和List<T>。因为我们其实不知道它俩具体在实现上的细节是如何的,还是比较一下这两个容器具体的性能比较好。比较的内容还是两项:下标访问及遍历。代码如下:

int length = 1000;
object value = new object();

ArrayList arrayList = new ArrayList(length);
for (int i = 0; i < length; i++)
{
    arrayList.Add(value);
    arrayList[i] = arrayList[i] ?? value;
}

List<object> list = new List<object>(length);
for (int i = 0; i < length; i++)
{
    list.Add(value);
    list[i] = list[i] ?? value;
}

CodeTimer.Initialize();
int iteration = 300 * 1000;

CodeTimer.Time("ArrayList下标访问", iteration, () =>
{
    for (int i = 0; i < length; i++)
    {
        var o = arrayList[i];
    }
});

CodeTimer.Time("List下标访问", iteration, () =>
{
    for (int i = 0; i < length; i++)
    {
        var o = list[i];
    }
});

CodeTimer.Time("ArrayList遍历", iteration, () =>
{
    foreach (object o in arrayList)
    {
        var o1 = o;
    }
});

CodeTimer.Time("List遍历", iteration, () =>
{
    foreach (object o in list)
    {
        var o1 = o;
    }
});

结果如下:

ArrayList下标访问
        Time Elapsed:   2,282ms
        CPU Cycles:     4,838,476,797
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0

List下标访问
        Time Elapsed:   2,302ms
        CPU Cycles:     4,979,267,920
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0

ArrayList遍历
        Time Elapsed:   5,187ms
        CPU Cycles:     11,145,830,014
        Gen 0:          4
        Gen 1:          0
        Gen 2:          0

List遍历
        Time Elapsed:   2,989ms
        CPU Cycles:     6,459,825,955
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0

现在您还觉得泛型会降低性能,List<Object>比ArrayList的性能差吗?

总结

从结果上已经可以看出,泛型并不会影响性能,而List<T>的性能也不比ArrayList要差。因此老赵继续坚持:在有泛型支持的情况下,尽量使用泛型容器。例如使用List<Object>而不是ArrayList。除了“性能”之外,老赵的还有其他一些理由。例如使用List<Object>的话就可以使用框架内部所定义的各种有用的辅助方法(要知道在.NET框架中,现在几乎都是在针对IEnumerable<T>进行开发);而我们平时写程序时,也可以统一的针对泛型编程,如IList<T>,IEnumerable<T>,不必考虑List或Enumerable等非泛型元素。

放心使用List<Object>吧。