首页 理论教育 性能优化-完美软件开发方法

性能优化-完美软件开发方法

更新时间:2025-01-19 工作计划 版权反馈
【摘要】:简单性有时候会和性能要求相冲突。很多的性能优化直接违反简单性原则,但又确实是必要的。当CUTOFF为8的时候,意味着在32位平台下,栈的进入次数不会大于30,在64位平台下栈的进入次数不会大于62。同时我们仅处理长度大于等于2的数组。因此检查是否有任何未排序的元素,并处理它们。算法本身和进栈、出栈这类操作其实没什么关系,但在优化后这种机制和算法被搅在了一起。这时候性能是有所损失的,但代码的清晰程度则可以得到提

简单性有时候会和性能要求相冲突。很多的性能优化直接违反简单性原则,但又确实是必要的。碰上这种情形的时候,唯一要做的事是弄清楚这么做的代价是什么,而又是否值得。

下面我们来看一个具体的例子。

在快速排序算法中,第一步要通过选定的值,把待排序的东西分为两组,一组比选定值大,一组比选定值小。接下来分别对已经区分开的两组值再根据选定的值进行分组,依次递归,最终完成排序。

假设qsort()函数的声明如下,其中base为待排序的数组,数组中的数据类型可以任意指定,left为数组第一个待排序元素的索引,right为最后一个待排序元素的索引,width为待排序元素的大小(以字节为单位),comp则是函数指针,指向一个对指定两个数组元素进行比较的函数。

如果elem1<elem2,那么返回小于0的值。

如果elem1=elem2,那么返回值等于0。

如果elem1>elem2,那么返回大于0的值。

这样基于教科书上的说明,qsort()很可能被实现成下面的这样子:

partition()负责根据选定值把整个数组划分为两组,一组比选定值小,一组比选定值大。返回值作为切分两组数据的元素的索引。具体实现可以参照相关算法书籍。

用上述方面实现的qsort()和通常所理解的快速排序算法一致,逻辑比较清晰。大多数人理解这种实现不需要花什么时间,但性能会偏慢—这在很多的时候对于类库是不能接受的。

接下来我们来看一下,现实中,做过优化的qsort()的实现方法。

下面代码来自微软的CRT Library,为了使主要逻辑更加清晰,去除了一些宏以及和算法本身没有关联的部分。考虑到这份代码比较不容易理解,所以把部分注释翻译了过来。

就qsort()函数自身而言,参数中有所变化的是只有一个num参数来表示数组中元素的个数。而不是要求待排序数组起始和终止元素的索引。

此参数用来定义一个边界值,如果待处理的元素个数大于这个值,那么使用快速排序,否则使用插入排序

理论上讲,栈的进入次数不会大于1+log2(num)。但由于我们会在元素个数小于等于CUTOFF值的时候启用插入排序,实际上栈的进入次数不会大于1+log2(num)-log2(CUTOFF)。当CUTOFF为8的时候,意味着在32位平台下,栈的进入次数不会大于30,在64位平台下栈的进入次数不会大于62。

伪递归调用的入口,设置lo和hi后跳转到这里就可以模拟递归。stkptr保存起来的,但其他局部变量没有被保存,所以我们用stack来保存数据。

下面是一个确定的数字,如果元素个数小于这个数字使用复杂度为O(n^2)的排序算法会更快。

首先我们要选出一个用来把数据分组的元素。这一算法的效率要求我们不止要选一个合适的中间值,还要尽快完成这一工作。我们选出第一个元素,中间元素以及最后一个元素的中间值,这一选择用来避免处理已排序数据的低效率,或者处理多组已排序数据组合的低效率。测试显示选择3个元素中间值的算法要比简单地选择中间元素具有更好的性能。

现在我们希望把数组中元素分为3组:一组小于等于中间元素,一组等于中间元素,另一组大于中间元素。下面的代码主要是完成这一工作,注释中写明了每一步的条件。(www.xing528.com)

下面的两次循环用于避免调用comp(mid,mid),这主要是因为有些现有的比较函数在收到两个同样指针的时候不工作。

下面的两次循环用于避免调用comp(mid,mid),这主要是因为有些现有的比较函数在收到两个同样指针的时候不工作。

在完成分组后,我们希望对[lo,higuy]和[loguy,hi]进行排序。为最小化栈的使用,我们先处理元素个数较少的。同时我们仅处理长度大于等于2的数组。

除了放入栈未进行处理的元素,我们已经完成了排序。因此检查是否有任何未排序的元素,并处理它们。

为了节省篇幅,一些辅助性的函数被忽略掉了,比如shortsort()和swap(),感兴趣的读者可以自行参照Microsoft CRT中的代码。

最初看到这种实现方法的时候,很多人都会感到困惑,不知道为什么递归不见了,反倒出现了跳转。事实上,出于速度优化的目的,这份代码用goto和模拟stack的数组实现了递归功能。stkptr用来模拟stack的指针,++stkptr的时候把数据压入stack,--stkptr的时候数据被从stack里取出来。

通过这种优化,运行时库里的实现比直接使用递归的实现,在性能上高出许多。但代价也十分明显,逻辑的清晰性被破坏掉了,增加了维护上的难度。

大多数人理解递归版的qsort()基本不会有障碍。但理解CRT版的qsort()就要花费较多时间,即使理解了,一段时间之后,再来阅读这份代码,也还是会有困难。而之所以这段代码理解困难,正是因为违反了我们之前所提出的逻辑链。

●违反正交原则。算法本身和进栈、出栈这类操作其实没什么关系,但在优化后这种机制和算法被搅在了一起。

这明显违反了我们所提倡的简单化原则,所以做这事情要由一个明确的支点,要有直接的需求做支撑。我们一直提倡的是:在满足既有需求的前提下,保证代码的简单。

而上述CRT的例子,则可以看做是满足需求所必须付出的代价。因为在受到广泛应用的基础库中,这种优化往往是值得的,也是必需的。

事实上,远不是只在基础库中才会出现这种需要进行取舍平衡的情况,在现实中需要在清晰性和性能间作平衡取舍的机会很多。

比如说,我们有一份记录某种信息的XML,需要对其中的信息进行指定的加工,而后转存为另外一份XML文件。

这个时候比较直接的方法是,用一个循环来依次遍历XML的各个节点,每获取一个节点的信息之后,就对该节点的信息进行加工,最终把所有加工后的信息存储到另一份文件。这个时候只需要一次循环。

这种实现方法的问题在于,如果XML每一个节点中的信息相对比较庞杂,那么信息加工的部分就会变得比较繁复。体现在实现上,一个可能的结果就是条件语句或case的增加。很类似7.3.2中所借用的没有重构前的statement()方法。

一个改善方法是,循环多次,每次只处理特定的节点,完成部分任务。这时候性能是有所损失的,但代码的清晰程度则可以得到提高。

对这类需要人员判定,而后进行选择的问题,是不可能找到标准答案的。只能根据现场的需求,才可能做出最终的取舍。单以实现而论,则应该是首先考虑逻辑清晰性,如果最终的程序确实会慢,那么再考虑性能优化。

现实中优化的方法有很多,比如,并行处理以减少读磁盘的次数、减少系统调用、优化算法等。这时候,以牺牲逻辑清晰性为代价,来优化算法这一选项,始终应该处在比较低的优先级上,只有真的必要的时候,才这么做。如果我们必须有所失去,那么至少我们要知道失去的是什么。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈