在我公开测试结果之后,有朋友也进行了其他测试。在测试中我使用的是int数组,经过分析之后我们了解到Array.Sort
性能测试
为了体现字段数量和排序时间的相关性,我们首先来构造一个方法,它可以使用Emit生成包含一定数量的字段:
public abstract class TypeBase { public int ID; } class Program { public static ModuleBuilder moduleBuilder = null; static Program() { var assemblyName = new AssemblyName { Name = "SomeAssembly" }; var domain = Thread.GetDomain(); var asmBuilder = domain.DefineDynamicAssembly( assemblyName, AssemblyBuilderAccess.Run); moduleBuilder = asmBuilder.DefineDynamicModule("SomeModule"); } static Type CreateType(int numberOfField) { var typeName = "TypeWith$" + numberOfField + "$Fields"; var typeBuilder = moduleBuilder.DefineType( typeName, TypeAttributes.Class, typeof(TypeBase)); for (int i = 0; i < numberOfField; i++) { typeBuilder.DefineField("Field$" + i, typeof(int), FieldAttributes.Public); } return typeBuilder.CreateType(); } }
方便起见,我让每种动态类型都继承统一的TypeBase类,这样我们排序的目标便可以定为TypeBase数组,而作为比较器的TypeBaseComparer也可以直接访问ID字段。然后便是测试用的方法:
static void Main(string[] args) { CodeTimer.Initialize(); var random = new Random(DateTime.Now.Millisecond); var array = Enumerable.Repeat(0, 1000 * 500).Select(_ => random.Next()).ToArray(); for (var num = 1; num <= 512; num *= 2) { var type = CreateType(num); var arrayToSort = new TypeBase[array.Length]; for (var i = 0; i < array.Length; i++) { var instance = (TypeBase)Activator.CreateInstance(type); instance.ID = array[i]; arrayToSort[i] = instance; } CodeTimer.Time( String.Format("Type with {0} fields (Array.Sort)", num), 10, () => Sort(CloneArray(arrayToSort))); CodeTimer.Time( String.Format("Type with {0} fields (ArraySorter)", num), 10, () => ArraySorter(CloneArray(arrayToSort))); } Console.ReadLine(); } static void Sort(TypeBase[] array) { Array.Sort(array, new TypeBaseComparer()); } static void ArraySorter(TypeBase[] array) { new ArraySorter<TypeBase>().OrderBy(a => a.ID).Sort(array); }
在比较时,我们将测试从1个字段的类型开始,每次将字段数量翻倍,直至512个字段——虽然夺得有些夸张,但对于试验来说,我还是希望差距能够明显一些。既然说Array.Sort
测试结果如下:
绘制成图表:
从图中可以看出明显的差别。当字段数量很少的时候,Array.Sort
这是为什么呢?在阅读以下内容时,您不如先自己思考一下?
原因分析
其实这还是一个和“局部性”有关的问题。局部性是影响性能非常主要的因素之一,我之前也不止一次谈过这个问题。那么在这里,局部性又是如何影响排序效率的呢?其实关键还是在于“比较器”上:
public class TypeBaseComparer : IComparer<TypeBase> { public int Compare(TypeBase x, TypeBase y) { return x.ID - y.ID; } }
TypeBaseComparer的实现非常简单,只是把两个TypeBase对象的ID值相减而已。显然,系统在执行这段程序的时候,会根据x或y的地址及ID字段的偏移量计算出一个地址,然后再去读取数据。不过我们知道,CPU在读取某个地址的的数据时,还会同时加载“一整条”的数据并缓存起来,这样再次读取附近的数据时会显得更快一些。那么试想一下,对于CPU来说,缓存及每个条目的大小是不变的,因此随着对象的体积增加,缓存中可以同时存在的对象数量便少了,这样虽然读取的次数不变,但是缓存的命中率就会随之下降。于是,对象体积增大,排序所消耗的时间也随之增加。
但是对于ArraySorter来说就完全不同了。根据ArraySorter的实现机制,它会首先根据keySelector得到排序字段——它在上例中就是个int值,IndexComparer然后在排序的时候将这个int数组保存起来,并且在排序时使用。因此,无论对象的体积是多少,在排序时ArraySorter永远只是在访问一个十分紧凑的int数组而已。排序结束后,ArraySorter也只是在操作一个个对象引用,它同样与对象的体积无关。由于排序其他条件不变,因此对象体积增大(造成局部性不佳)最终也只是导致keySelector工作的时候速度变慢,而其他部分统统不受影响。自然从某一时刻开始ArraySorter的性能会超过Array.Sort
如果您对现在谈论的内容不很理解的话,可以了解一下与“局部性”有关的内容,并了解一下LINQ排序及ArraySorter的实现方式。
总结
可见,Array.Sort
当然,一些明显的错误是需要避免的。例如,您在比较器中反复访问数据库的话,这就是您自己的问题了。
此外,我们这篇文章着重于测试和分析,但是如果可以直观地观察到“局部性”相关的数据岂不是更能说明问题吗?那么我们有什么办法通过实验来证明这一点呢?这问题其实也不大。例如,在使用Visual Studio 2008的Profiler时,我们可以让它同时监视L2 Cache的情况。这个实验的设计和执行就交由您亲自来吧。
经过了这几篇文章,您对于.NET框架中的排序类库,是否还有什么疑惑呢?
本文代码:http://gist.github.com/288765
相关文章
- 数组排序方法的性能比较(1):注意事项及试验
- 数组排序方法的性能比较(2):Array.Sort
实现分析 - 数组排序方法的性能比较(3):LINQ排序实现分析
- 数组排序方法的性能比较(4):LINQ方式的Array排序
- 数组排序方法的性能比较(5):对象大小与排序性能