您的位置:首页 > 编程语言 > Python开发

一个小项目中的Python中的性能优化细节——(上)从排序说起

2015-03-22 14:00 645 查看

0 概述

虽然和C相比,python的效率低一些,但是这 并不是说python中就不需要考虑效率问题了。

最近在写一个文本预测的工具的时候,需要建立一个二阶马尔可夫模型。我在最开始的版本中,以一个4M大小的文本作为原材料。分割过程需要10S以上,hash表的建立需要15秒,而排序(快排)则因各种原因没有排出来。所以我将目光转向了一些没有注意的地方。

事实证明,cProfile的运用会让我发现一些没有注意到的细节,而优化它们真的会为程序的运行节省很多时间!

到了目前的版本,分析7个总大小为20M的文本并建立数据索引,花费的时间是:分割20s、markov构建22s。相比于之前4M总耗时20秒以上,整体加速了200%!

这个优化的过程真的是很有意思也很有成就感的,因此,在下面详细的记录一下我认为重要的条目。代码地址

1 快排并不快?——快排的优化和改进

最基本的快排,qsort1

最开始的版本中,因为python的字典不像C++ 的set,基于红黑树实现,因此我需要对一个字典进行排序。

首先为了方便,我写了一个insert sort函数。在小文本规模的情况下,也就是对一个A+B词组的下一个可能词组成的字典排序,这个排序是可用的(甚至于再后来的比较中发现效率还很高)。

但是在我对全部的A+B序列排序时,这个O(n^2)的算法显然不能胜任排列15W个元素的工作(这是预料之中的)

因此我必须使用快排。这个版本称之为QSORT1,它的代码很简单,就是书本上最常见的——

while(i<j){while(i<j&&condition1) j--; assignment1 while(i<j&&condition2) i++; assignment2}


这样的形式。

O(nlgn)的算法应该很快了吧?结果却不尽人意,因为在30秒后还没有排完!虽然是间接访问,但是这个时间显然已经超出了合理的范围。一定是哪里出问题了

快排拓展之一:相等情况处理,Qsort2

QSORT1失败后,我对整个要排序数组的性质做了思考。

首先,这个数组规模最大可能达到1000(文本为圣经,其中大量出现the lord这样的词组);

其次,这个数组中有大量元素是相等的!

我之前常使用快排,但也仅限于C优化后的的qsort函数、或者自己写的简单快排,并没有认真的思考对==情况的处理会对性能有多么大的不同?

因此,基于以上两点,我写出了下面的这个程序:

def __quicksort__(self,top,end):
if top > end:
return
index_rand = top #norandom random.randint(top,end)
flag = self.list_DictKeys[index_rand] ;
i = top ; j = end
write_top = top ; write_end = end
list_Equal = []
list_Equal.append(flag)

while(i<j):
while(i<j):
if self.diction[self.list_DictKeys[j]] < self.diction[flag]:
self.list_DictKeys[write_end] = self.list_DictKeys[j]
j -= 1
write_end-=1
elif self.diction[self.list_DictKeys[j]] == self.diction[flag]:
list_Equal.append(self.list_DictKeys[j])
j-=1
else:
break
self.list_DictKeys[write_top] = self.list_DictKeys[j]
self.list_DictKeys[i] = self.list_DictKeys[j]

while(i<j):
if self.diction[self.list_DictKeys[i]]>self.diction[flag]:
self.list_DictKeys[write_top] = self.list_DictKeys[i]
i+=1
write_top += 1
elif self.diction[self.list_DictKeys[i]] == self.diction[flag]:
list_Equal.append(self.list_DictKeys[i])
i += 1
else:
break
self.list_DictKeys[write_end] = self.list_DictKeys[i]
self.list_DictKeys[j] =  self.list_DictKeys[i]

len_list_Equal = len(list_Equal)
for index_Equal in range(len_list_Equal):
self.list_DictKeys[index_Equal + write_top] = list_Equal[index_Equal]
self.qsort_stack.append( [ top, write_top-1 ] )
self.qsort_stack.append( [ write_end+1, end ] )


思想很简单,但是改的时候还真是BUG百出。如果你觉得被上面的这个复杂的数据结构绕蒙了的话(我就是),不妨看看下面的这一段。

while(i<j):
while(i<j):
if a[j] < flag:
a[write_end] = a[j]
j -= 1
write_end-=1
elif a[j] == flag:
list_Equal.append(a[j])
j-=1
else:
break
a[write_top] = a[j]
a[i] = a[j]
while(i<j):
if a[i]>flag:
a[write_top] = a[i]
i+=1
write_top += 1
elif a[i] == flag:
list_Equal.append(a[i])
i += 1
else:
break
a[write_end] = a[i]
a[j] =  a[i]


我花费了一些时间去整理快排简单代码背后的一些逻辑,将其中的一些整合部分分解,终于理清了整个的过程。(当然我也花费了一些时间,理清了那个复杂的数据结构)

结果证明,效率的提升是可观的。15W的规模花费时间0.3秒

结论1—— 对于有大量重复元素的集合排序,等号处理对于性能有极大的提升作用!

快排拓展之二——递归改循环,栈的引入

为什么要自己维护一个函数栈?因为我在处理15W数据的时候栈溢出了!

可以计算一下,15W(约等于2^17)在最差情况下产生2^17个函数栈,一个函数栈占用4Byte(也可能是8Byte),那么。。。好吧我怎么算出上G了?

上面的版本快排并没有使用递归,而是使用了一个list容器实现栈。

对快排的认识仅仅在写出正确代码的同学,可能没有想过递归转换成循环。不过看了下面的代码,相信你会觉得不过如此嘛!

def qsort(a):
qsort_stack.append([0,len(a)-1])
while qsort_stack != [ ] :
top_end = qsort_stack.pop()
top = top_end[0]
end = top_end[1]
__quicksort_random__(a,top , end )
return a
是的,思想很简单,每个子函数尾部生产两个子栈,主循环不断的消耗这些子栈,虽然效率略微降低,但是极大节省了栈空间

快排拓展之三——random版本。

如果一个集合,恰好有序,还恰好是相反的顺序,那么快排就悲剧了!

所以我们有时候需要一个random机制来避免这种悲剧发生。(不要想random后恰好是最差情况,这种情况可以认为是不可能事件)

从概率的角度来讲,random后快排应该是处于大于O(nlgn)复杂度、但是远小于n2复杂度的性能。

那么,问题来了,随机化的快排究竟是不是适合我们的字典排序?

我认为不适合。

在对4M文本处理的时候,效果还不明显,甚至于random版本会有一些提升; 但是在20M版本中,random函数却消耗了1秒的时间

结合我们要处理队列——乱序字典 它本身属于有序的概率实在是太小了!

如果可以的话,能够研究一下python 的dict机制是再好不过的了,不过如果python不用红黑树的dic都能比C++基于红黑树的set,那C++就哭晕在厕所了。。。

所以我删去了提升并不明显、甚至因为频繁调用而消耗大量时间的random。

2.insert排序真的慢吗 ?——合理选择排序

无脑快排的代价

我对上面的那个改进后的快排很满意,所以很快的把它运用到了各种需要排序的地方。
这样一来,结果应该快很多了吧?可是测试的时候,却发现,频繁调用快排的版本却比insert版本还要慢!
为什么会这样?
在对数据分析的时候我发现,后缀的元素个数大于100的词组,只有3000个左右,而其余的200W个词组,则大多处于20以下!
这就意味着:我们对于完全不需要快排的地方,调用了快排,反而因为频繁的函数调用大大降低了效率!

插入排序带来的神奇

这时候无脑快排真的是太傻了!为什么不插入排序呢?
我为此做了一个测试,10W次快排和插入排序,对于这个项目中的字典,词缀个数在90~110的区间内,二者时间是差不多的!
因此就有了下面的代码:
if len(dict_NextWord) < 80:
list_NextWord_sorted = c_DictSorter(dict_NextWord).insertsort()
else:
list_NextWord_sorted = c_DictSorter(dict_NextWord).qsort()
原本排序会花费12秒,现在只需要1.6+1.8的时间了!

结论:快排不是唯一选择,插排有时也是神器。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐