0.033秒的艺术 --- 二叉树遍历
仅供个人学习使用,请勿转载,勿用于任何商业用途。
最近在写一些关于树的操作,显然最常见的树操作就是遍历所有节点,而遍历节点,最常见的算法自然是递归。下面是最初的二叉树实现和遍历
老实说,我觉得这样的实现很简洁,而且性能也不错,循环1000次遍历一棵高度为8的完全满二叉树(255个节点),大约0.0015秒。不过作为程序员,我们总想让代码跑的更快,所以来看看还可以对上面的代码做什么优化。
首先,可以改用数组来实现二叉树,节约2个指针的开销,遍历树的算法也可以简化为遍历数组。可惜我还需要处理一些包含n个可变节点数的树型结构,所以这样的优化似乎没有通用性。看来只能对遍历算法进行改进了。虽然几乎所有关于数据结构的书里都会用计算阶乘来演示递归,代码大全里却指出用递归实现阶乘其实是最低效的算法:大量函数调用会带来性能损耗,而且无法预期运行期间的内存实用状况。Real-time collision detectiong也指出手动建栈遍历树要比递归高效。下面是修改之后的遍历代码:
虽然比递归版本要复杂一些,不过不再有大量函数调用,那么性能如何呢?还记得以前说过永远不要在测试之前下结论吗,结果大大出乎我的意料,测试结果:非递归版本居然要比递归版本慢3~5倍!!我最先认为,就算非递归版本慢,差距也不会很大。问题出在哪里呢?上面的函数每次调用都会创建新的stack对象,循环1000次,就产生了1000个垃圾对象。我把stack移动到了函数外,作为静态变量,再次测试,结果还是令人失望,没有任何性能提升。用CLRProfile测试性能,发现在节点数为4000左右时,stack居然分配了5mb内存,而4000个节点的实际内存消耗只有几百k左右。这也许是因为stack内部用array来保存变量,每次当元素数量超过数组大小时,需要重新分配一个更大新数据,并且拷贝数据,以满足需要。是这个原因拖慢了程序的运行速度吗?创建时显式为stack 分配了足够大的空间,再用CLRProfile查看,这次stack的内存占用正确了,可惜性能还是没有提升。
内存分配上应该没问题了,于是改用DotTrace查看究竟是哪一部操作耗费了如此多时间:
终于找到瓶颈了,整个程序居然有近60%的时间都耗费在了stack操作,再次出乎我的意料,没想到.net的stack居然这么慢!
当然,还可以用unsafe code重新实现stack,来看到底非递归版本是否会有性能提升。由于正在开发的是类库,所以我希望尽量不使用unsafe code,而且最初的版本似乎已经可以满足需求。
至此,我对遍历的优化完全失败了-_- #,但如果不做测试,又如何知道哪种方法才是最好的呢,至少我们知道了.net里的stack并没有想象中的那么快:)