个人项目-词频统计

开发语言:

C#

开发平台:

Visual Studio 2013 Professional

预计时间:

建立工程基本框架:半小时

模块-递归寻找所有文件:半小时

模块-扫描&分离单词:一个半小时

Debug&优化:两小时

实际时间:

预计时间x3

事实证明,预计时间是建立在一个相当顺利的基础上才能达到的。在实际Coding中,由于对C#文件等操作不熟悉,以及扩展模式时更改框架,还有思路混乱等等,花掉了不少时间,这些时间应该要加入预计当中的。

主要遇到的问题:

1.字符串比较问题

C#默认并不依照ASCII码字典序比较字符串,而是与语言&文化相关,也就是在Windows资源管理器里对文件名排序时用的比较方法,a是排在A前面的。解决这个问题有很多方法,以下是其中一种:

比较字符串:string.CompareOrdinal(s1, s2)

忽略大小写判断字符串是否相等:string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase)

这个Ordinal我真的不知怎么翻译。

2.控制台与文件流的统一接口

我在设计output方法时,引入参数StreamWriter,这样就可以选择在debug的时候将输出定位到Console了。但是翻遍了Console的成员,尝试好多都失败了。不过最后还是找到了办法,将参数类型改为TextWriter,由于Console.Out就是这个类的,而StreamWriter是TextWriter的继承类,所以统一了两者的接口。至于两个Writer有什么区别,暂时没有深究。

3.分离单词

一开始想用正则表达式的,折腾好久总算写了个感觉正确的,但还是有问题:题目要求单词前后都是分隔符,但是正则表达式查找时分隔符之间不能覆盖。比如说“hello world”只能识别hello,而“hello      world”就能识别hello和world。

没办法,还是老老实实逐字符处理吧。

4.扩展模式实现

我的框架是按照标准模式设计的。标准模式实现之后,加入扩展模式,必然要更改一些模块。主要更改如下:

* 命令行参数判断,不用多说

* 将“单词表”直接作为”单词组表”,排序方法不变,之所以这么做,是因为扩展模式在语义上本来就不是标准功能的一部分,既然如此,List不标准让人误会又有何妨?况且,这是成本最低的做法。

* 独立一个CurrentWord方法,用以寻找当前位置是否存在一个单词,如果有,则返回。作业要求e2或e3模式下单词分离符只能是一个单独的空格,所以必须手动判断两个或三个单词,将CurrentWord模块化是必须的。

* 输出相应处理

5.CPU采样

一开始用简单文件测试,选择Sampling模式时,弹出一个窗口

stackoverflow上有人给出的解释是,由于进程一下子就运行完了,VS还没来得及采集数据,所以出现了报错。

解决办法是更改为Instrumentation模式,或者让程序变慢些(-_-#)。

测试数据: 

 

# 测试目的 描述 输出 备注
1   未指定扫描文件夹 控制台:Please specify
a directory!
 
2   参数错误:-e4 控制台:The argument must be -e2 or -e3. Scanning cancelled.  
3   文件夹不存在 控制台:The directory
specified doesn't exist!
 
4   文件夹为空 空文件  
5 验证单词判定&分离 一个txt文件,内容:
hello #kitty 3english中too    xxx12 xx
second hello
aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp qqq
hello: 2
aaa: 1
bbb: 1
ccc: 1
ddd: 1
eee: 1
fff: 1
ggg: 1
hhh: 1
iii: 1
jjj: 1
kitty: 1
kkk: 1
lll: 1
mmm: 1
nnn: 1
ooo: 1
ppp: 1
qqq: 1
second: 1
too: 1
xxx12: 1

输出了所有单词
6 验证统计&排序 一个txt文件,内容:
hello Hello heLLo yyy XXX xxx xxx
Hello: 3
XXX: 3
yyy: 1
 
7 验证文件类型 若干个文件,内容都为:
hello Hello heLLo yyy XXX xxx xxx
文件类型分别为:
txt, cpp, h, cs, png, (null)
Hello: 12
XXX: 12
yyy: 4
 
8 验证递归寻找文件 根目录下是一个文件和一个目录,目录下是一个文件和一个目录,目录下又是一个文件。
三个文件都是txt,内容一致:
hello Hello heLLo yyy XXX xxx xxx
Hello: 9
XXX: 9
yyy: 3
 
9 验证扩展模式-e2 单个txt文件,命令行参数-e2
if you do not learn to think when you are young you may never learn Edison
zzz zxz you You yOu you you #like     that
You yOu: 4
are young: 1
learn Edison: 1
may never: 1
never learn: 1
not learn: 1
think when: 1
when you: 1
you are: 1
you may: 1
1.只列出前10个
2.按单词频度顺序
3.视为相同的单词组,选择字典序最先者:You yOu
4.连续单词计数:4个you you
5.分隔符只能是单个空格(you like和like that不在列,它们本应该排在you may的前面
10 验证扩展模式-e3 单个txt文件,命令行参数-e3
if you do not learn to think when you are young you may never learn Edison
zzz zxz you You yOu you you #like     that
You yOu you: 3
are young you: 1
may never learn: 1
never learn Edison: 1
think when you: 1
when you are: 1
you are young: 1
you may never: 1
young you may: 1
zxz you You: 1
只列出前10个(zzz zxz you未出现)

如果说要保证自己程序是正确的,这几乎不可能。就算能做到,推理也不是一时三刻能完成的。

优化:

程序扫描得相当慢,出乎我的意料。我用The Kite Runner原版小说(500+k)扫描,预计要40分钟左右。

我提取了前面的325行,CPU Sampling测试结果:

总时间15秒。

看起来是一个Count方法占用了大多数的时间,定位到代码:

原来是每次判断字符串长度时,都调用方法计算!

我将所有Count()方法改成了Length属性,再分析:

时间缩短至1.5秒左右,是原先的十分之一。性能提升至1000%!

我再用The Kite Runner测试,几秒钟就出来了。速度快了不止一星半点。

接下来对Visual Studio 2013文件夹进行测试,这个目录共2.78GB

时间竟花了20+分钟,耗时最多的是统计文件中单词个数的函数,占了65%:

原来我在判断这个单词是否已统计过的时候,对已有单词列表中的每一项都去判断。大多数的判断其实是多余的,比如Hello应该和hello去比较,而不必和world、kitty什么的再比较了。

要解决这个问题,我的思路是将单词列表更改为有序字典,key是单词的小写形式,value是单词信息类。

每次插入时,都判断这个单词是否在列表中。由于是有序的,可以利用二分查找。

(为什么不直接用有序表呢?那是因为,最终输出的顺序是综合考虑频度和单词ascii码等等,而这个顺序不可能作为维护有序表时的顺序)

最终测试,扫描VS文件夹只花了32秒,性能提升至4000%!

(另外再说一下,由于这个文件夹放在SSD里,成绩参考意义不大)

最终算下来,经过两个优化,程序快了400倍!

 

优化性能往往意味着增加代码复杂度,有的地方我并没有深入优化。下面给出优化和未优化的部分:

已优化的部分:

* 利用查询表达式“推迟执行”的特性,一边遍历文件一边统计该文件的单词数。(时间)

* 上述两个优化

* 构建单词时,使用StringBuilder类。(时间)

未实现的优化:

* 直接对流文件处理,而不是先把内容读到一个string里。(空间&时间)——个人感觉流的指针移动操作比较复杂,况且时间提升不会太大,而空间呢,文本文件一般也不会太大。

* 用正则表达式替代手工判断。(时间)——也许效率能提升不少

* 对于扩展模式,只找出Top10,而不是全部排序。(时间)——不优化的话,反而可以保证标准和扩展模式的时间复杂度是一致的

* 并行计算。(时间)——不会

* 还有很多我没想到的……

总结:

* 预计项目时间时,要考虑到进行中可能遇到的阻力,比如Google、Debug所花的时间,这些往往比敲代码更加耗时。

* 警惕方法调用,能用字段(属性)就不要调用方法。比如这次Count()和Length达到的目的相同,但性能却差了10倍。

* 有时候性能上的问题,仅凭观察代码是很难发现的,这时使用性能分析工具是最好的选择。

posted @ 2014-09-24 00:37  Xrst  阅读(624)  评论(1编辑  收藏  举报