个人项目总结 (By Jun Guo)
为期两周的个人项目就这样过去了。虽然说还有很多不足的地方,但自己还是比较满意吧,毕竟是认真做了的。
项目挺简单的,就是做一个词频统计程序,用来统计文章里各个单词出现的次数。可以说,这已经简单到不能称为“项目”了。不过,写这个程序前前后后还是遇到挺多问题——毕竟,要将效率优化到极致实在是很麻烦的事情。
一开始秋丰老师给出超级指标——要让程序能处理10G数据。当时一看到这个指标我就各种囧,因为万一10G数据里所有单词都不同(即每个单词的词频都为1),这堆单词连直接存放在内存中都做不到。为了解决这个问题,程序就不得不在处理了部分数据后就将该部分的词频统计结果保存到文件上,最后再将这些文件合并输出结果。将数据分开来处理倒还不是很困难,真正难的部分在于合并。所谓的合并指的是,比如A文件里记录单词hello出现了1次,B文件里记录单词hello出现了2次,那我们要将结果合并为hello出现了3次。为了在较低的复杂度下完成合并,程序就需要先将各个文件整理为有序,最后再采用多路合并将这些文件综合在一起得到结果——这本质上就是外部排序。众所周知,外部排序的效率与磁盘块大小、磁盘缓存大小、文件系统缓存大小等息息相关,为了达到最高效率,代码必须调用Win 32 API来完成有关的检测。
于是,在秋丰老师超级指标的压迫下,一份极其复杂的外部排序代码诞生了。然而,在做测试时我就发现,虽然程序对付大数据确实非常高效,但对付小数据实在力不从心——对于一份只有几M的文件,采用简单的哈希或红黑树,在内存中完成对所有单词的统计显然会比优化到极致的外部排序还要快得多。一想到要针对不同大小的文件写不同算法,还要通过大量测试来选定一个最佳的阈值(超过阈值就切换算法),我的心顿时就发毛了……于是,果断联系了秋丰老师。在打扰了秋丰老师N次之后,终于得到一个肯定的答复——测试数据中不同单词数不会超过100万个。这总算让我舒了一口气,终于不用去寻找最优阈值了;但这也同时宣告那堆外部排序的代码可以进垃圾桶了……
就这样,重建一个工程,开始用哈希表来解决词频统计的问题。既然提到哈希表,那首先就要选定一个优秀的哈希函数。字符串哈希印象中效果比较好的有ELF Hash、BKDF Hash这几个。我生成了些数据,测试一下各个哈希函数的碰撞率,同时记录下各个哈希函数进行哈希运算的时间开销,最后发现BKDF Hash无论是碰撞率还是哈希开销都远远优于其他函数,于是BKDF Hash就成必然选择了。
之后要将哈希函数运用到容器上。C++0x下的STL多了个unorder_map,是一个基于哈希的无序容器,理论上能提供 比红黑树的map更优的查找与插入性能。不过,根据我以前写代码的经历,STL的各种容器向来都是低效的代表,这个unorder_map估计也是如此。经过测试,果然很坑爹慢……算了,反正自己写个哈希表也不麻烦,于是就动手写了。为了容易解决冲突,用了开放式链表法;为了减少new操作的开销,又将开放式链表法里的链表换成数组链表。
除此以外,为了减少那缓慢的string所带来的开销,还重写了string类……
除了还有一些微不足道的最小堆优化等等,程序基本上也没什么好说的了。事实上,一开始在采用哈希表之前,我也考虑过用字典树来解决。但后来一想,字典树是O(length)插入,哈希表也是O(length)插入,而且字典树的插入常数与基于BKDF Hash的哈希表基本一样(对于每个字符,字典树是一次判断一次赋值外加一次new,BKDF是一次乘法一次加法外加一次赋值;字典树的new操作不一定发生,但一旦发生开销极大——虽然我也试过用内存池或者预先分配来改进之,但效果不明显,有时反而更慢),再加上字典树有爆内存的风险,因此最后并没有采用(代码留着,但没有调用)。
最后经过秋丰老师的测试,在普通机械硬盘上,340M的数据大概4s多就跑完,平均1s能处理80M数据,可以说瓶颈已经不在程序而是在硬盘上了。这还是让我较为满意的。
PS 1.
按秋丰老师要求,我在完成这个个人项目时也统计了下代码行数和开发时间。如今对比一下完成项目前的时间预估,发现结果很搞笑。一开始我预计大概要写1500行代码,总的开发时间则预估为50小时;但最后总共才写了627行代码,总的开发时间却花了29小时。虽然真实的开发时间比预计的要少,但从单位时间所编写的代码量来看,原有预计还是过高估计了产出效率了……不过,能提前完成任务也是件不错的事情。
PS 2.
有人问我为什么没有用C#。确实啊,我是个重度C#控,为何在上文一直没提到C#呢?呃,其实原因是——我在做个人项目前升级了.Net Framework,结果不小心给安装失败了,不但新装的.Net Framework 4.5用不了,连以前的.Net Framework 2.0/3.5/4.0一律都用不了。而我当时又忙着看论文,所以没有时间去检查安装失败的原因,于是只能蛋疼地用C++了……