代码改变世界

据说ArrayList真的比List<T>快?测试一下下。

2009-06-14 14:05  JimLiu  阅读(4346)  评论(28编辑  收藏
据说刚高考完,我们写作文要向伟大辛苦的毕业班同学们看齐,所以一篇记叙文应该有……

时间:就刚才。

地点:我寝室。

人物:我。

起因

最近我好像跟“性能”、“效率”这些敏感词汇较上劲了,先汗一个。

因为某些奇异的原因,我又做了如下极端无聊的事情,那就是对List<T>与ArrayList的“性能”的一个小测试。

在这个测试中,重点在于对List<T>与ArrayList的[Add,遍历]性能作讨论,坚决不将话题牵扯到普通容器和泛型容器。

测试中用到了老赵一个简单的性能计数器——CodeTimer

由于值类型存在boxing/unboxing,泛型容器有得天独厚的优势,二者几乎不存在可比性,所以我没做值类型的测试。

最终测试结果都包含Debug/Release两个数值。

经过

这里我用的元素类型是我自己定义的一个类,其定义如下

元素类型定义
class Class {
   
int a, b, c, d, e, f, g;
}

用于测试的容器有两个:System.Collections.ArrayList,以及System.Collections.Generic.List<Class>

容器定义为

容器定义
ArrayList arrayList = new ArrayList();
List
<Class> list = new List<Class>();

首先是测试Add的性能

测试代码是:

插入测试代码
Class obj = new Class(); // 用这个来插入
int n1 = 60 * 10000// 插入次数


CodeTimer.Time("ArrayList.Add", n1, () => {
    arrayList.Add(obj);
});

CodeTimer.Time(
"List<Class>.Add", n1, () => {
    list.Add(obj);
});

插入测试结果:

插入结果Debug
ArrayList.Add
        Time Elapsed:   36ms
        CPU Cycles:     
59,072,030
        Gen 
0:          1
        Gen 
1:          1
        Gen 
2:          1
List
<Class>.Add
        Time Elapsed:   26ms
        CPU Cycles:     
41,694,040
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0
插入结果Release
ArrayList.Add
        Time Elapsed:   35ms
        CPU Cycles:     
54,320,590
        Gen 
0:          1
        Gen 
1:          1
        Gen 
2:          1
List
<Class>.Add
        Time Elapsed:   22ms
        CPU Cycles:     
36,750,110
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0

可以看出List<Class>比ArrayList插入快一些。

下面就是大家比较关注的遍历性能测试了。

这里有两种遍历,一种是for,一种是foreach。容器内的东西就是刚才插入的那一堆。

下面是测试代码:

遍历性能测试用例
int n2 = 60 * 5; // 遍历次数

CodeTimer.Time(
"ArrayList for(i)", n2, () => {
    
for (int i = 0; i < arrayList.Count; ++i) {
        var tempObj 
= arrayList[i];
    }
});


CodeTimer.Time(
"List<Class> for(i)", n2, () => {
    
for (int i = 0; i < list.Count; ++i) {
        var tempObj 
= list[i];
    }
});


CodeTimer.Time(
"ArrayList foreach", n2, () => {
    
foreach (Class it in arrayList) {
        var tempObj 
= it;
    }
});


CodeTimer.Time(
"List<Class> foreach", n2, () => {
    
foreach (Class it in list) {
        var tempObj 
= it;
    }
});

呃……然后是遍历的结果

遍历结果Debug
ArrayList for(i)
        Time Elapsed:   
3,194ms
        CPU Cycles:     
5,250,408,900
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0

List
<Class> for(i)
        Time Elapsed:   
3,124ms
        CPU Cycles:     
5,156,644,380
        Gen 
0:          0
        Gen 
1:          0

        Gen 
2:          0

ArrayList foreach
        Time Elapsed:   
5,911ms
        CPU Cycles:     
9,800,432,750
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0
List
<Class> foreach
        Time Elapsed:   
2,971ms
        CPU Cycles:     
4,867,982,990
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0
遍历结果Release
ArrayList for(i)
        Time Elapsed:   
1,647ms
        CPU Cycles:     
2,697,106,390
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0

List
<Class> for(i)
        Time Elapsed:   652ms
        CPU Cycles:     
1,084,824,510
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0

ArrayList 
foreach
        Time Elapsed:   
5,377ms
        CPU Cycles:     
8,884,062,410
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0
List
<Class> foreach
        Time Elapsed:   
1,163ms
        CPU Cycles:     
1,925,787,250
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0

这个结果有点出乎我意料,在Debug模式下,ArrayList和List<Class>几乎平手,差别在2%左右,foreach上ArrayList则完全败北。在Release模式下却发生了很大的变化:for方面List<Class>拥有非常惊人的优化幅度,ArrayList的for虽然也快了很多但是依然不及List<Class>;foreach方面ArrayList停滞不前,而List<Class>却进一步拉大差距。

考虑到有时候我们会这么做

事先获取长度的for遍历
int arrayListLen = arrayList.Count;

CodeTimer.Time(
"ArrayList for(i)", n3, () => {
    
for (int i = 0; i < arrayListLen; ++i) {
        var tempObj 
= arrayList[i];
    }
});
int listLen = list.Count;

CodeTimer.Time(
"List<Class> for(i)", n3, () => {
    
for (int i = 0; i < listLen; ++i) {
        var tempObj 
= list[i];
    }
});

这是我们的老师常常教我们的一种方法。结果如下:

事先获取长度的遍历结果Debug
ArrayList for(i)
        Time Elapsed:   
2,438ms
        CPU Cycles:     
3,975,979,180
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0
List
<Class> for(i)

        Time Elapsed:   2,470ms
        CPU Cycles:     
4,056,331,770
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0
事先获取长度的遍历结果Release
ArrayList for(i)

        Time Elapsed:   1,182ms
        CPU Cycles:     
1,962,797,610
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0
List
<Class> for(i)

        Time Elapsed:   552ms
        CPU Cycles:     
917,996,780
        Gen 
0:          0
        Gen 
1:          0
        Gen 
2:          0

在Debug模式下,ArrayList终于“雪耻”,以大约1.3%的微弱优势超过了List<Class>,但很可惜,到了Release下List<Class>没有给ArrayList留下任何的机会。

结果

在这个测试所能涵盖的范围内,我们找不到任何理由为ArrayList树立功德碑,List<Class>以压倒性的优势告诉我们选择它是正确的。

后话

对于值类型,泛型容器的效率是有目共睹的,我不想做那种比这个无聊的测试还无聊的无聊测试了。

对于这个测试中的for测试,对ArrayList是不公平的,因为那一句var tempObj = arrayList[i];编译器显然会把var编译成object,而我们期待的是Class,实际应用中这里势必要发生type casting,所以ArrayList的for在实际中效率负担还会多上那么一丁丁点。

对于这个测试中的foreach测试,对ArrayList还是不公平的,因为我们foreach的是(Class it),对于ArrayList内部的IEnumerable实现来说,我们从这个代码中很难说ArrayList做了怎样的type casting,而对于List<Class>来说这个type casting我们是知道,它显然是不用做的。

但是话说回来,ArrayList的type casting不就是我们讨厌的地方吗?那么还有什么理由不选择List<T>