相关文章推荐
正直的皮蛋  ·  腾讯红魔游戏手机6S ...·  1 月前    · 
买醉的拖把  ·  碧蓝航线:微速前行!·  1 年前    · 
个性的红茶  ·  飞机融资租赁_百度百科·  1 年前    · 
烦恼的上铺  ·  Poem: A Longing Wife ...·  1 年前    · 
visits

昨天我们 比较了Array.Sort<T>方法与LINQ排序的性能 ,知道了LINQ排序的性能以较大幅度落后于Array.Sort<T>方法。而对于Array.Sort<T>来说,性能最高的是其中使用Comparer<int>.Default作为比较器的重载方法。在前文的末尾我们做出了推测:由于排序算法已经近乎一个标准了(快速排序),因此从算法角度来说,Array.Sort<T>方法和LINQ排序上不应该有那么大的差距,因此造成两者性能差异的原因,应该是具体实现方式上的问题。

下载.NET框架的代码

既然是比较实现的区别,那么阅读代码是很直接的选择。谈到阅读.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文件,不过这是另一个话题了,我们现在只关心源代码。

因此,我建议您将所有微软提供的源代码都下载到本地。在看不懂.NET Reflector的反编译结果时,不妨参考一下源代码——还是包含注释的源代码。

Array.Sort<T>方法实现

各Array.Sort<T>方法的重载最终都会委托给下面这个重载来执行:

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):对象大小与排序性能
Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment 43 条回复

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)。举例说,如果有类似这样的代码:

interface IFoo { void Bar(); }
class Foo1 : IFoo { public void Bar() { } }
class Foo2 : IFoo { public void Bar() { } }
//...
var array = new IFoo[] {
  new Foo1(), new Foo1(), // ...(50个Foo1实例)
  new Foo2(), new Foo2(), // ...(150个Foo2实例)
foreach (var f in array) {
  f.Bar();
}


那么f.Bar()的f是Foo1时就一直比较快,过了会儿那个调用点的f都是Foo2了,速度就骤然降了下来并退化到megamorphic状态……
头3轮循环的时候那个stub还处于收集类型信息的初始状态,等到第3轮发现receiver都是Foo1类型的,就开始创建特化的monomorphic inline cache,然后Foo1的状况就特别快。等到receiver变成Foo2为主的时候,每次f.Bar()调用都使得一个计数器加一,那个计数器到100了就把调用点转变到megamorphic状态,于是……

Jeffrey Zhao:

姜敏:
话说"LINQ的确有优势啊",是和我的测试一致,还是打问号的一样,嘎嘎,希望有朋友和我的测试结果有相同的人啊,同时我修改了代码,大家认为我的测试linq排序时,实际只排序了一次,我在循环中重新new一个,但结果一样,linq还是强,大家可以拿我的代码重新测试.


看了你的代码,你的基本概念啊……
1、你还是没有CloneArray,复制了引用什么都不算的,你测试LINQ排序还是没有包括CloneArray的开销。
2、说你只排序一次的人也不正确,因为LINQ本身就是不影响原有序列的,不过CloneArray的开销还是需要的。


老赵,我的测试代码中也包含你的代码,你的那部分我就没有修改过,但结果还是和你不同.

姜敏:

Jeffrey Zhao:
@姜敏
这样吧,你确认你是Release编译,并且不是在VS里按F5,或者确定VS里的Suppress JIT Optimization取消掉了吗?默认是打开的。



第一:在release下,linq强一些,如果是debug就强太多了.

第二:我是按的F5运行的.


第三:VS里的Suppress JIT Optimization没有修改,话说在哪修改成关闭?

Tools -> Options... -> Debugging -> Suppress JIT optimizations on module load (Managed only)

F5运行跟Ctrl+F5运行的性能表现不同是正常现象

其实Debug版本没有太多的参考价值,我看了Debug版本的那个Compare方法的IL,充斥这大量莫名其妙和无用的东东:

.method public hidebysig newslot virtual final
instance int32 Compare(class Exam11.Person x,
class Exam11.Person y) cil managed
{
// 代码大小 23 (0x17)
.maxstack 2
.locals init ([0] int32 CS$1$0000)
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldflda int32 Exam11.Person::ID
IL_0007: ldarg.2
IL_0008: ldfld int32 Exam11.Person::ID
IL_000d: call instance int32 [mscorlib]System.Int32::CompareTo(int32)
IL_0012: stloc.0
IL_0013: br.s IL_0015
IL_0015: ldloc.0
IL_0016: ret
} // end of method PersonComparer::Compare

Jeffrey Zhao:

姜敏:
话说"LINQ的确有优势啊",是和我的测试一致,还是打问号的一样,嘎嘎,希望有朋友和我的测试结果有相同的人啊,同时我修改了代码,大家认为我的测试linq排序时,实际只排序了一次,我在循环中重新new一个,但结果一样,linq还是强,大家可以拿我的代码重新测试.


看了你的代码,你的基本概念啊……
1、你还是没有CloneArray,复制了引用什么都不算的,你测试LINQ排序还是没有包括CloneArray的开销。
2、说你只排序一次的人也不正确,因为LINQ本身就是不影响原有序列的,不过CloneArray的开销还是需要的。

他那篇文章中的代码的问题并不是linq只排序一次,而是用linq排序的List在之前已经用List<T>.Sort排过序了,也就是说用linq做的是对一个有序的List进行排序,而用List<T>.Sort的那段代码才是只有第一次排序是真正的排序,其余几次都是对有序List再“排序”。
另外,我觉得用linq倒是没必要CloneArray,因为linq本身不影响原列表,关键是测试Array.Sort的时候不应该把CloneArray的开销算进去。

@麒麟.NET

麒麟.NET:
@RednaxelaFX
我想问问RFX大大,你是看了哪些书才了解了这么多的呢?


请叫我FX谢谢……
我的知识面是在语言实现方面特化的,在这个领域内的知识我所知道的也不算多。这些都不是*你必须知道的.NET*的知识,作为应用程序员花很多时间去“知道”这些的意义并不大,因为基本上用不到。换了问我数据库优化或者socket编程或者ASP.NET或者语音识别之类的我就一头雾水了……

其实有几本相关的非常经典的书我都还没来得及读,像是Smalltalk-80的blue book和讲CLOS的MetaObject Protocol的。我读过的一些VM相关的强悍的书有例如《Shared Source CLI Essentials》的第一版(这个我有原版;第二版一直没出实体书,只有PDF的草稿)、《Virtual Machines: Versatile Platforms for Systems and Processes》(这个买了影印版)。

不过与VM的实现相关的许多信息都“藏”在论文堆中,而没有成书。

ECMA-335和JVM规范讲的都是规范而不是实现,要读前者的话配合《The Common Language Infrastructure Annotated Standard》比较好;
《Inside the Java Virtual Machine》同样讲的是规范所定义的概念;
《Essential .NET, Vol 1》讲了一些实现不过书本身非常老了(但仍然非常值得.NET程序员一读);《CLR via C#》跟这个是同一类,也非常值得.NET程序员一读;
《Virtual Machine Design and Implementation in C/C++》与《Virtual Machines》(Iain Craig那本,这本我也有原本但觉得好亏)其实都没用到什么强悍的实现技巧。后者好歹还提了一下threaded code,但inline caching之类的主题根本没提及。
《Distributed Virtual Machines: Inside the Rotor CLI》这本我还没怎么读,里面的状况不是太清楚。
《Garbage Collection》这本虽然老但是必读,读了之后会发现它的思想到现在都还不老(注意,是思想不老,不是实现不老)。
《Advanced Compiler Design and Implementation》(俗称鲸书)这本里讲到的一些技巧可以用在JIT的实现中。这本我读的是中文版,感觉还挺顺的。当然讲编译原理的书里提到的优化技巧普遍可以用在JIT中,但这本讲到了些具体的有趣的。

书里没讲,此类知识要淘就得到论文堆里了。关于Self的实现的论文一大堆都值得读,Oberon相关的也非常值得读,后面就到大量的讲JVM实现的论文也值得读,还有些别的VM例如PyPy、TraceMonkey的也值得读。要读这些论文最好是有个ACM帐号,这个要是在大学里的话就不是问题,已经毕业了的话就拜托一下学弟学妹们帮忙吧。……或者自己有钱买个帐号更好。
读blog也行,有不少blog里会有零散的相关信息,要找的话请自行放狗。找起来会挺吃力的。
然后就是源码,很多知识藏在代码的注释里。许多人很强悍的想法未必有时间写成详细的文档,但在代码里或许会有不错的注释,多读读也没坏处(在有时间的前提下,嗯时间……)
再不行就上逆向工程吧。虽说对有些商业产品的逆向工程属于灰色地带,但在天朝出于自我学习的目的还是可以合法做很多事情的orz。我对CLRv2的了解主要是通过读CLR开发者的blog、参考SSCLI 2.0与自己写例子做逆向工程来获取的。

这篇blog里,有你很多的评论回复,老赵的排序都是用 array([ ])的形式,我把他传数组的地方都改成了List,运行了发现性能还是 Linq快,这是为什么呢?

我的部分修改代码:

var array = Enumerable.Repeat(0, 1000 * 500).Select(_ => random.Next()).ToArray(); List<Person> list = new List<Person>(); Person p; for (int i = 0; i < 1000 * 500; i++) p = new Person(); p.firstName = string.Format("{0} firstName", array[i]); p.lastName = string.Format("{0} lastName", array[i]); p.ID = array[i]; list.Add(p);

上面代码,生成List 实例

static List<Person> CloneArray(List<Person> source) var dest = new Person[source.Count]; var sour = source.ToArray(); Array.Copy(sour, dest, source.Count); sour = null; return dest.ToList<Person>();

上面代码CloneArray,把List 转换成Array,再复制,最后变回List。

static void SortWithCustomComparer(List<Person> array) array.Sort(new PersonComparer()); public class PersonComparer : IComparer<Person> public int Compare(Person x, Person y) return x.ID-y.ID;

我的Peson类

public class Person public string firstName; public string lastName; public int ID; CodeTimer.Initialize(); // CodeTimer.Time("SortWithDefaultComparer", 100, // () => SortWithDefaultComparer(CloneArray(list))); CodeTimer.Time("SortWithCustomComparer", 100, () => SortWithCustomComparer(CloneArray(list))); // CodeTimer.Time("SortWithDelegate", 100, // () => SortWithDelegate(CloneArray(list))); CodeTimer.Time("SortWithLinq", 100, () => SortWithLinq(CloneArray(list))); CodeTimer.Time("SortWithCustomComparer", 100, () => SortWithCustomComparer(CloneArray(list))); CodeTimer.Time("SortWithLinq", 100, () => SortWithLinq(CloneArray(list)));

这里主函数调2遍,查看输出:

并且 Suppress JIT Optimization取消,是Release版本,直接运行exe,结果还是Linq的快一点。

我这里主要的不同点是在 函数参数主要是传List参数,一个最大的差别是在 CloneArray(List source),我每次都是Linq快一点,这是为什么?

赵劼,网名老赵,洋名Jeffrey Zhao,花名赵姐夫,金融行业程序员,目前就职于摩根大通(香港)。多年微软MVP,InfoQ中文站兼职编辑。虽然广泛涉猎各类技术,但早就被人民群众贴上“微软”标签,现已认命。

经过多年寻觅起伏,目前已打算潜心在金融行业发展。关注前沿技术,热爱开源。对函数式编程,并行程序开发,语言设计与实现,代码之美以及程序员能力与修养等相关问题也有着浓厚的兴趣,非常希望能够写程序到60岁。热爱生活,在技术之余也时常健身做饭弹钢琴,立志为彰显码农正面形象做出贡献。

希望可以给初学者以合适引导。坚定的北大青鸟反对者,强烈愤慨恶劣的培训机构对于处于懵懂期的初学者以误导,强烈抵制各种虚假广告给业界带来的不良影响,强烈建议有理想有抱负的从业青年放弃北大青鸟,不要做冤大头。

本站所有内容均代表个人观点,与本人雇主及其他团体无关。

  • 防止装箱落实到底,只做一半也是失败
  • 为什么我不喜欢Go语言式的接口(即Structural Typing)
  • 为什么我认为goroutine和channel是把别的平台上类库的功能内置在语言里
  • 如何在不装箱的前提下调用“显式”实现的接口方法?(答案)
  • 如何在不装箱的前提下调用“显式实现”的接口方法?
  • 针对struct对象使用using关键字是否会引起装箱?
  • 串与绳(1):.NET与Java里的String类型
  • Everpage:将Evernote的笔记展现在页面上
  • 真是O(1)吗?想清楚了没?
  • 一个最基本的HashedLinkedList
  • 阅读.NET源代码那些事
  • NullableKey:解决Dictionary中键不能为null的问题
  • 不同泛型参数区分的独立类型
  • 更多...
  • 如何生成一段Typoglycemia文本(解答)
  • 如何生成一段Typoglycemia文本?
  • 如何让您的事件支持逆变
  • 为List<T>内部添加一个“删除多个元素”的方法
  • 图灵访谈之三十六:以“玩”之名——赵劼(老赵)专访
  • 讨论:一则并行聚合计算方案的设计
  • 我看面试时出(纯)算法题
  • 由eval生成的代码效率真的很差吗?
  • 我对“语言之争”的看法:别随便拉我入场
  • 写给《程序员》杂志社:那些你们早该知道的东西
  • 专访Jscex作者老赵(上):缘由、思路及发展
  • Jscex与Promise/A那些事
  • 使用Node.js编写Shell脚本,暨Jscex 0.6.5版本发布
  • 两年多来第二次更新博客功能
  • Jscex单元测试:喝着咖啡品着茶
  • Jscex预编译器及其DocPad插件
  • Jscex疯狂周末
  • C#的设计缺陷(2):不能以void作为泛型参数
  • C#的设计缺陷(1):显式实现接口内的事件
  • 编写一个“绑定友好”的WPF控件
  • Erlang Haskell Python Scala Cassandra CouchDB FluidDB Hypertable MogileFS MongoDB MySQL PostgreSQL Redis Terracotta Tokyo Cabinet Hadoop IKVM.NET IronPython IronRuby Irony Netty NHibernate NutBox Nutch C9 Lectures: Dr. Erik Meijer - Functional Programming Fundamentals Channel 9: Expert to Expert Channel 9: The Visual Studio Documentary MIT 6.00 - Introduction to Computer Science and Programming MIT 6.001 - Structure and Interpretation of Computer Programs MIT 6.046 - Introduction to Algorithms The Channel 9 Highlights UC Berkeley: Data Structures 编程语言的发展趋势及未来方向 F#中的异步及并行模式 “可测试性”驱动开发 ASP.NET Routing相关 HTML内容生成 NHibernate自定义集合类型 表达式树与反射调用 并发环境下的缓存容器性能优化 从.NET中委托写法的演变谈开去 基础性能相关 基于Routing库的URL生成方式 老赵谈IL 浅谈代码的执行效率 浅谈尾递归 浅谈线程池 视图片断缓存 谈表达式树的缓存 谈网页静态化 我对NHibernate的感受 挣脱浏览器的束缚 重谈字符串连接性能 重提URL Rewrite