• 个人项目-词频统计


    开发语言:

    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倍。

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

  • 相关阅读:
    JavaScript的3种继承方式
    JavaScript回调函数及数组方法测试
    JavaScript实现二叉树算法
    SpringMVC之使用Servlet原生API作为参数
    HashMap详解
    面试笔记--Fast-Fail(快速失败)机制
    面试笔记--HashMap扩容机制
    org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1
    多模块调用Service失败
    常用命令汇总
  • 原文地址:https://www.cnblogs.com/xrst/p/3989660.html
Copyright © 2020-2023  润新知