经常看到有人因为使用.net中的集合类处理海量数据时性能不够理想,就武断的得出.net不行,c#也不行这样的结论。对于.net framework这样的类库来说,除了性能以外,通用性和安全性同样重要,而为了后者,有时就不得不牺牲性能。如果你的程序核心就是处理大量数据集合,并且对.net内置类库性能不满意,那么这时候就应该考虑为特定类型实现一个优化的版本了。
      事情的由来是我需要对若干个(<10)集合进行排序,每个集合中的元素不会超过2k,老实说,所要处理的数据并不多,但我希望在1ms之内完成所有操作,这就成了挑战。也许有人觉得1ms的要求有些苛刻,但对我的应用来说,1ms已经很些奢侈了. 所要处理的数据非常简单,大部分都是一些数据集对象,每个对象都有一个uint类型的id,并以此为key进行排序:

  1. public class KeyValuePair
  2. {
  3.     public uint id;
  4.     public string s = string.Empty;
  5.     public float d;
  6.     public double c;
  7. }

复制代码

集合大小是不固定的,List自然就是最合适的容器,并且有现成的方法用于排序List.Sort()。我使用了以Comparison委托为参数的版本,并定义了如下函数:

  1. public static int Compare(KeyValuePair v1,KeyValuePair v2)
  2. {
  3.     return v1.id.CompareTo(v2.id);
  4. }

复制代码

我知道,这是一个非常不规范的Comparison定义,应该先检查对象是否为null等等。不过这里只是测试而已,我只想看看List.Sort最快能到什么程度。测试数据为一个有5k元素的List,和5个每个有1k元素的List。通过多次测试,以减少噪音干扰,结果都在2~3ms之间,令人印象非常深刻,足以满足大部份应用的需求。可惜对我的目标来说,还是差了一点。
      于是我决定自己写一个函数。List.Sort内部使用了Array.Sort,而后者的实现则是传统快速排序(quicksort)算法。在众所周知的几种排序算法中,quicksort是平均情况下最好的,因此我将仍旧使用这一算法,只是删除List.Sort内一些不必要的检查。代码如下:

  1. public static void QuickSort(List<KeyValuePair> list, int start,int end)
  2. {
  3.     if (end <= start)
  4.         return;
  5.     int pivotIndex = FindPivot(list, start, end);
  6.     swap(list, pivotIndex, end);
  7.     int k = Partition(list, start, end, list[end].id);
  8.     swap(list, k, end);
  9.     QuickSort(list, start, k - 1);
  10.     QuickSort(list, k + 1, end);
  11. }
  12. public static int Partition(List<KeyValuePair> list, int start, int end, uint piovtValue)
  13. {
  14.     start--;
  15.     while (true)
  16.     {
  17.         do { start++; }
  18.         while (list[start].id < piovtValue);
  19.         if (start > end)
  20.             break;
  21.         do { end--; }
  22.         while (end > 0 && list[end].id > piovtValue);
  23.         if (start > end)
  24.             break;
  25.         swap(list, start, end);
  26.     }
  27.     return start;
  28. }
  29. public static int FindPivot(List<KeyValuePair> list, int start, int end)
  30. {
  31.     int a = (int)list[start].id;
  32.     int b = (int)list[end].id;
  33.     int middle = (start+end)/2;
  34.     int c = (int)list[middle].id;
  35.     if ((a - b) * (a - c) < 0)
  36.         return start;
  37.     if ((b - a) * (b - c) < 0)
  38.         return end;
  39.     return middle;
  40. }
  41. public static void swap(List<KeyValuePair> list, int a, int b)
  42. {
  43.     KeyValuePair temp = list[a];
  44.     list[a] = list;
  45.     list = temp;
  46. }

复制代码

这里并没有使用任何特别的技巧,几乎是按数据结构书中的例子照搬过来。它的性能如何呢?以下是测试结果:
list size    list.sort    myQuickSort
5k            2ms            4ms
10k            6ms            5ms
100k          58ms          26ms
200k          132ms          58ms
    非常有趣,当列表元素小于10k时,list.sort比myQuickSort快,而随着元素的增加,myQuickSort将快2~4倍。第二种情况是意料之中的,用reflector查看list.sort的源码就能发现,我的quickSort实现显然要简洁很多。但为什么当元素不多时,会出现如此明显的反差呢?起初我尝试用DotTrace分析两个函数所执行的时间,不幸的是由于此时数据太少,排序执行的太快,DotTrace分析的结果是完全错误的: 总是显示quickSort比list.sort所用的时间少。看来只有查看IL了,于是以下代码引起了我的注意:
IL_0002:  callvirt  instance !0 class [mscorlib]System.Collections.Generic.List`1<class Console4Test.KeyValuePair>::get_Item(int32)
      这是swap函数中,访问列表元素所产生的代码。过去,我一直认为访问list元素和访问数组元素是相同的,此时,我开始有所怀疑了。居然出现了callvirt这样的指令,虽然在IL中,callvirt并不意味着一定是虚函数调用,reflector也证明List.get_item并不是一个虚方法。直觉告诉我应该对这行代码深究下去,看看JIT究竟把它编译为了什么样的指令:

  1. swap(list, k, end);
  2. 000000c7  cmp        eax,dword ptr [esi+0Ch]
  3. 000000ca  jb          000000D1
  4. 000000cc  call        787E9A3C
  5. 000000d1  mov        eax,dword ptr [ebp-14h]
  6. 000000d4  mov        edx,dword ptr [esi+4]
  7. 000000d7  cmp        eax,dword ptr [edx+4]
  8. 000000da  jae        0000015E
  9. 000000e0  mov        eax,dword ptr [edx+eax*4+0Ch]
  10. 000000e4  mov        dword ptr [ebp-20h],eax
  11. 000000e7  cmp        edi,dword ptr [esi+0Ch]
  12. 000000ea  jb          000000F1
  13. 000000ec  call        787E9A3C
  14. 000000f1  mov        eax,dword ptr [esi+4]
  15. 000000f4  cmp        edi,dword ptr [eax+4]
  16. 000000f7  jae        0000015E
  17. 000000f9  mov        eax,dword ptr [eax+edi*4+0Ch]
  18. 000000fd  mov        dword ptr [ebp-24h],eax
  19. 00000100  mov        eax,dword ptr [ebp-14h]
  20. 00000103  cmp        eax,dword ptr [esi+0Ch]
  21. 00000106  jb          0000010D
  22. 00000108  call        787E9A3C
  23. 0000010d  mov        ecx,dword ptr [esi+4]
  24. 00000110  push        dword ptr [ebp-24h]
  25. 00000113  mov        edx,dword ptr [ebp-14h]
  26. 00000116  call        78F1B384
  27. 0000011b  inc        dword ptr [esi+10h]
  28. 0000011e  cmp        edi,dword ptr [esi+0Ch]
  29. 00000121  jb          00000128
  30. 00000123  call        787E9A3C
  31. 00000128  mov        ecx,dword ptr [esi+4]
  32. 0000012b  push        dword ptr [ebp-20h]
  33. 0000012e  mov        edx,edi
  34. 00000130  call        78F1B384
  35. 00000135  inc        dword ptr [esi+10h]

复制代码

这段代码确实让人有些惊奇。首先,swap函数被内联了,这正是我们所希望的;其次,list元素访问也被正确内联了,没有发生我们之前担心的函数调用。这里确实有几条call指令,不过这是在发生异常才会调用的地址。另外,虽然这里没有列出,但值得一提的是FindPivot中的list元素访问则没有内联,每次访问list元素都意味着执行一次函数调用以及这个函数中的16条汇编指令。 最后,2个简单的list元素交换竟然产生了30条以上的汇编代码,我想这也是所有人所料未及的。
      看来list元素访问确实是一个潜在的问题。为了证明这一点,我把quickSort中,所有list都改为了array。再次测试,果然,我自己的版本无论在任何情况下都比list.sort快,同时,也比array.sort快。这里不再列出实际的测试数据,只贴出array版本的swap汇编代码:

  1.             swap(list, k, end);
  2. 00000070  cmp        eax,dword ptr [esi+4]
  3. 00000073  jae        000000BE
  4. 00000075  mov        eax,dword ptr [esi+eax*4+0Ch]
  5. 00000079  mov        dword ptr [ebp-1Ch],eax
  6. 0000007c  mov        ecx,dword ptr [esi+edi*4+0Ch]
  7. 00000080  mov        eax,dword ptr [ebp-10h]
  8. 00000083  lea        edx,[esi+eax*4+0Ch]
  9. 00000087  call        78F11F98
  10. 0000008c  push        dword ptr [ebp-1Ch]
  11. 0000008f  mov        edx,edi
  12. 00000091  mov        ecx,esi
  13. 00000093  call        78F1B384

复制代码

可以看到,代码减少为了原来的1/3。两个call同样是.net内部的一些安全检查代码。
        好了,现在知道我的代码慢在哪里了,但这并不能解释list.sort为什么在元素少的时候比较快,难道它不受list元素访问效率的影响吗?是的,list本身并不会受到自身元素访问机制的影响,因为他调用Array.sort时,传递的是内部储存的私有元素数组成员,而不是他自己。因此,可以猜测,当元素较少时,排序算法执行的非常快,此时,元素访问方式的不同,就成了明显的瓶颈,而当处理元素较多时,大部分时间都用在排序上,元素访问的代价则逐渐变小。
以上手写的quickSort方法还能进一步优化吗?显然是可以的:
1,当quickSort中的分组元素小于10时,改用插入排序,可以带来大约5~10%的性能提升:

  1. public static void QuickSort(List<KeyValuePair> list, int start,int end)
  2. {
  3.     if (end <= start)
  4.         return;
  5.     int pivotIndex = FindPivot(list, start, end);
  6.     swap(list, pivotIndex, end);
  7.     int k = Partition(list, start, end, list[end].id);
  8.     swap(list, k, end);
  9.     if (k - start <= 10)
  10.         InsertSort(list, start, k - 1);
  11.     else
  12.         QuickSort(list, start, k - 1);
  13.     if (end - k - 1 <= 10)
  14.         InsertSort(list, start, k - 1);
  15.     else
  16.         QuickSort(list, k + 1, end);
  17. }

复制代码

2,把FindPivot函数手动内两到QuickSort中。
3. 用栈模拟递归,本人不是太推荐这种做法。
4. 也许还能用指针优化关键操作,不过似乎c#不允许对reference type使用指针L
小结:
在极端性能要求下,需要对元素进行排序时:
当n< ~5k时,用array代替list,或者为array写一个简单的wrapper,并且自己实现sort;或者直接使用list.sort
当 n > 10k时, 实现自己的sort方法,至少能得到2~4倍的提速。

List.Sort以及快速排序ZZ的更多相关文章

  1. 快速排序算法回顾 --冒泡排序Bubble Sort和快速排序Quick Sort(Python实现)

    冒泡排序的过程是首先将第一个记录的关键字和第二个记录的关键字进行比较,若为逆序,则将两个记录交换,然后比较第二个记录和第三个记录的关键字.以此类推,直至第n-1个记录和第n个记录的关键字进行过比较为止 ...

  2. js实现冒泡排序(bubble sort)快速排序(quick sort)归并排序(merge sort)

    排序问题相信大家都比较熟悉了.用js简单写了一下几种常用的排序实现.其中使用了es6的一些语法,并且不仅限于数字--支持各种类型的数据的排序.那么直接上代码: function compare (a, ...

  3. [算法]——快速排序(Quick Sort)

    顾名思义,快速排序(quick sort)速度十分快,时间复杂度为O(nlogn).虽然从此角度讲,也有很多排序算法如归并排序.堆排序甚至希尔排序等,都能达到如此快速,但是快速排序使用更加广泛,以至于 ...

  4. 排序算法—快速排序(Quick Sort)

    快速排序(Quick Sort) 快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序. ...

  5. 【算法】快速排序(Quick Sort)(六)

    快速排序(Quick Sort) 快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序. ...

  6. javascript版快速排序和冒泡排序

    var sort = (function () { //快速排序 var quickSort = { partition: function (array, low, high) { if (low ...

  7. 快速排序之python

    快速排序( Quick sort) 快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行递归排序,以达到整个序列有 ...

  8. C++——sort和stable_sort的若干区别

    版权声明:本文系作者原创,转载请注明出处. C++中sort和stable_sort的区别: sort是快速排序实现,因此是不稳定的:stable_sort是归并排序实现,因此是稳定的: 对于相等的元 ...

  9. 洛谷 P1177 【模板】快速排序(排序算法整理)

    P1177 [模板]快速排序 题目描述 利用快速排序算法将读入的N个数从小到大排序后输出. 快速排序是信息学竞赛的必备算法之一.对于快速排序不是很了解的同学可以自行上网查询相关资料,掌握后独立完成.( ...

随机推荐

  1. [Effective JavaScript 笔记]第23条:永远不要修改arguments对象

    arguments对象并不是标准的Array类型的实例.arguments对象不能直接调用Array方法. arguments对象的救星call方法 使得arguments可以品尝到数组方法的美味,知 ...

  2. iATKOS v7硬盘安装教程(硬盘助手+变色龙安装版)

    这是作者:Tong 写的一篇安装教程 首先感谢:wowpc制作的变色龙安装版.iATKOS作者以及硬盘安装助手作者 前言:现在时代在进步,系统同样也在进步,在以前要在PC上整个Mac是很痛苦的事情,就 ...

  3. [BZOJ1659][Usaco2006 Mar]Lights Out 关灯

    [BZOJ1659][Usaco2006 Mar]Lights Out 关灯 试题描述 奶牛们喜欢在黑暗中睡觉.每天晚上,他们的牲口棚有L(3<=L<=50)盏灯,他们想让亮着的灯尽可能的 ...

  4. 原生Android动作

    ACTION_ALL_APPS:打开一个列出所有已安装应用程序的Activity.通常,此操作又启动器处理. ACTION_ANSWER:打开一个处理来电的Activity,通常这个动作是由本地电话拨 ...

  5. chm文件打开空白无内容的解决办法

    今天下载了个chm文件,但是打开空白,也没显示什么内容,经过一番研究之后终于可以正常显示了,下面把解决办法分享出来供大家参考下,谢谢.   工具/原料 windows7系统 chm文件 方法/步骤   ...

  6. FastCgi与PHP-fpm之间是个什么样的关系

    刚开始对这个问题我也挺纠结的,看了<HTTP权威指南>后,感觉清晰了不少. 首先,CGI是干嘛的?CGI是为了保证web server传递过来的数据是标准格式的,方便CGI程序的编写者. ...

  7. Sublime Text 2 入门及技巧

    看了 Nettuts+ 对 Sublime Text 2 的介绍, 立刻就兴奋了,诚如作者 Jeffrey Way 所说:“<永远的毁灭公爵>都发布了,TextMate 2 还没发”,你还 ...

  8. 1-2+3-4+5-6+7......+n的几种实现

    本文的内容本身来自一个名校计算机生的一次面试经历,呵呵,没错,你猜对了,肯定 不是我 个人很喜欢这两道题,可能题目原本不止两道,当然,我这里这分析我很喜欢的两道. 1.写一个函数计算当参数为n(n很大 ...

  9. Git 怎样保证fork出来的project和原project(上游项目)同步更新

    1.  在 Fork 的代码库中添加上游代码库的 remote 源,该操作只需操作一次即可. 如: 其中# upstream 表示上游代码库名, 可以任意. git remote add upstre ...

  10. jquery 常用类别选择器

    1.$('#showDiv'):  id选择器,相当于javascript中的documentgetElementById("showDiv"); 2.$("onecla ...