既然是比较实现的区别,那么阅读代码是很直接的选择。谈到阅读.NET代码,我们往往会使用.NET Reflector将框架的程序集反编译为C#代码——不排除有朋友喜欢观察IL,并认为它们更直接,更底层。不过我倒觉得在绝大部分情况下,
IL能看出的东西从C#也能看出
,
而C#无法了解的IL也帮不上忙
,因此许多“由IL发现问题”的文章其实只是自己和自己在爽。
不过,虽然.NET Reflector将程序集反编译成非常优秀的C#代码,但是还是无法恢复之前的不少信息。例如,局部变量名无法得知,这就给理解代码意图造成了困难。再例如,为了可读性我们可能会将一些条件语句分开写,而C#编译器则可能把它们连做一块儿。至于注释等明显会被去除的东西就更不用说了。因此,在能直接读到代码的情况下,我并不倾向于使用.NET Reflector。
您可能会说:这不是废话么,不过有些类库——如.NET框架并没有提供源代码,这又有什么办法呢?其实微软也已经公布了.NET框架相当部分程序集的源代码(几乎所有v2.0的程序集,如mscrolib,System,System.Web等等),而且它们其实都可以使用
NetMassDownloader
直接下载到本地。随员代码发布的还有方便调试的pdb文件,不过这是另一个话题了,我们现在只关心源代码。
public static void Sort<T>(T[] array, int index, int length, IComparer<T> comparer)
if (length > 1)
// <STRIP>
// TrySZSort is still faster than the generic implementation.
// The reason is Int32.CompareTo is still expensive than just using "<" or ">".
// </STRIP>
if (comparer == null || comparer == Comparer<T>.Default)
if (TrySZSort(array, null, index, index + length - 1))
return;
ArraySortHelper<T>.Default.Sort(array, index, length, comparer);
我们知道,从逻辑上说,Array.Sort<T>方法会使用IComparer<T>类型的比较器来判断两个元素的大小。不过在这里,.NET框架作了一个“特例”,它在用户没有提供比较器,或是直接使用默认比较器的时候利用TrySZSort方法进行排序。如果TrySZSort方法如果返回true,则表示排序完成,直接返回。如果它返回false,则继续使用内置的排序方法进行处理。那么TrySZSort又是如何实现的呢?
private static extern bool TrySZSort(Array keys, Array items, int left, int right);
这是一个extern方法,说明它是由CLR直接实现的,我们无法得知它的具体算法或是意图。不够从注释中可以得知,这个做法的目的是提高性能(明白注释的优势了吧)。每次使用IComparer<T>的Compare方法进行比较的时候相当于是一次虚方法的调用,CLR需要计算它的偏移量,也无法将其内联。这个细节相对于直接进行int的大小比较来说,也是有较大开销的。使用TrySZSort这种外部方法进行排序,有助于提高在特定情况下的执行效率。
因此,我们应该可以有足够信心来推断出TrySZSort的作用。TrySZSort方法的作用是对一些可以直接进行比较的原生类型(如int等)进行排序,如果它发现自己无法支持数组中元素的类型,那么就返回false,否则便排序后并返回true。如果TrySZSort返回false,便会使用ArraySortHelper进行排序。而其中的实现便是标准(真的很标准)的快速排序,如果您感兴趣的话可以自行阅读其中的代码。
值得注意的是,Array是定义在System命名空间下的类型,而ArraySortHelper则定义在System.Collections.Generic命名空间下。在阅读微软提供的源代码时有个麻烦之处便是不容易导航,例如ArraySortHelper类便让我一顿好找。不过,其实我们也可以配合.NET Reflector中强大的导航功能来快速定位到某个类的位置,然后直接去查找它对应的文件。当然,如果您索引了文件内容,也可以查找类似“class ArraySortHelper”这样的关键字,也可以很快找到特定文件。
Array.Sort<T>其他重载的性能
以上,我们已经知道为什么使用Comparer<int>.Default作为比较器时性能最高了,因为对于int类型来说,Array.Sort<T>方法会使用最快的TrySZSort方法进行排序。而如果我们使用自定义的Int32Comparer进行比较的话,Compare方法调用的开销是无可避免的,根据实验结果,使用Int32Comparer的执行时间比前者有80%的增加也可以接受。
那么使用委托进行排序的时候为什么比Int32Comparer更慢一些呢?且看其代码:
public static void Sort<T>(T[] array, Comparison<T> comparison)
IComparer<T> comparer = new FunctorComparer<T>(comparison);
Sort<T>(array, comparer);
其实原因很简单,因为.NET框架使用了FunctorComparer封装了comparison委托。这样在每次比较时,它相对于Int32Comparer来说还增加了委托执行的开销——这又相当于一次虚方法的调用:需要寻址,无法内联。
至此,我们已经分析了Array.Sort<T>各重载的实现方式,也了解了三种Array.Sort<T>重载性能有别的原因。刚好,不久前姜敏兄又回应了我的文章,认为使用Person类,而不是int类型进行比较的时候,仍旧是LINQ排序比较快——由此他认为两种做法的性能和类型也有关系。您认为这个说法正确吗?其实从实现上看,Array.Sort<T>几乎已经是最优的做法了。相反,LINQ排序由于会生成额外的序列,性能想要超过Array.Sort<T>几乎是件不可能的事情。但事实真是如此吗?
那这测试结果……我也写了一个Person类的测试(http://gist.github.com/282796),还是Array.Sort<T>比较快。那么为什么两个结果会有所不同呢?这是一个值得探讨的问题。
数组排序方法的性能比较(1):注意事项及试验
数组排序方法的性能比较(2):Array.Sort<T>实现分析
数组排序方法的性能比较(3):LINQ排序实现分析
数组排序方法的性能比较(4):LINQ方式的Array排序
数组排序方法的性能比较(5):对象大小与排序性能
43 条回复
赵劼,网名老赵,洋名Jeffrey Zhao,花名赵姐夫,金融行业程序员,目前就职于摩根大通(香港)。多年微软MVP,InfoQ中文站兼职编辑。虽然广泛涉猎各类技术,但早就被人民群众贴上“微软”标签,现已认命。
经过多年寻觅起伏,目前已打算潜心在金融行业发展。关注前沿技术,热爱开源。对函数式编程,并行程序开发,语言设计与实现,代码之美以及程序员能力与修养等相关问题也有着浓厚的兴趣,非常希望能够写程序到60岁。热爱生活,在技术之余也时常健身做饭弹钢琴,立志为彰显码农正面形象做出贡献。
希望可以给初学者以合适引导。坚定的北大青鸟反对者,强烈愤慨恶劣的培训机构对于处于懵懂期的初学者以误导,强烈抵制各种虚假广告给业界带来的不良影响,强烈建议有理想有抱负的从业青年放弃北大青鸟,不要做冤大头。
本站所有内容均代表个人观点,与本人雇主及其他团体无关。
hmm...又可以灌点水了
在.NET里,数组可以分成SZArray和非SZArray两种。SZ就是single-dimensional, zero-based index,也就是我们一般用的数组。非SZ的数组可以是一维的但下标不从0开始(在C#里得调用特别的方法才能创建者种数组,没有专门语法支持),也可以是多维数组(也就是ElementType[a,b,c]形式的)。很多快速的数组例程都是只针对SZ数组,所以……不过平时大家甚少用非SZ数组,所以没啥特别需要注意的。
TrySZSort()的作用是对元素类型为CLR直接支持的原始类型的SZ数组做快排;如果元素类型没有直接支持则返回false。
在CLRv2里调用接口方法的开销并不总是很大,许多时候只是比调用普通的虚方法细微慢一点而已。在调用接口方法的调用点上会记录receiver的类型信息,同一类型的receiver在一个接口方法调用点上出现3次就会使那个调用点变为一个monomorphic inline cache,然后在累积出现100次预测错误时退化到megamorphic状态。
例如说有这样的IFoo接口:
interface IFoo { void Bar(); }
class Foo : IFoo { public void Bar() { } }
那么在这样一个调用点:
foo.Bar();
如果发现这个foo一直是Foo类型的,那么那个调用点逻辑上就会变成:
if (foo.GetType().TypeHandle == typeof(Foo).TypeHandle) {
call Foo::Bar()
} else {
cacheMiss++;
if (cacheMiss >= 100) GotoMegamorphic();
lookup IFoo::Bar() and call
}
这是monomorphic状态,也就是假设经过这个调用点的receiver总是同“一”类型的。
Megamorphic状态的话则不做这种特例检查,而是通过一个特化的hashtable来做查询和跳转。
可惜的是在CLRv2里,这种方法的inline cache一旦退化到megamorphic状态后就无法回到monophormic状态了,因而无法有效的处理所谓程序的阶段迁移(phase shift)。举例说,如果有类似这样的代码:
那么f.Bar()的f是Foo1时就一直比较快,过了会儿那个调用点的f都是Foo2了,速度就骤然降了下来并退化到megamorphic状态……
头3轮循环的时候那个stub还处于收集类型信息的初始状态,等到第3轮发现receiver都是Foo1类型的,就开始创建特化的monomorphic inline cache,然后Foo1的状况就特别快。等到receiver变成Foo2为主的时候,每次f.Bar()调用都使得一个计数器加一,那个计数器到100了就把调用点转变到megamorphic状态,于是……