个人项目——计算器之小学生
一.项目时间规划与实际用时
PSP2.1 |
Personal Software Process Stages |
预计时间/h |
实际时间/h |
Planning |
计划 |
|
|
· Estimate |
· 估计这个任务需要多少时间 |
10 |
10 |
Development |
开发 |
|
|
· Analysis |
· 需求分析 (包括学习新技术) |
2 |
2 |
· Design Spec |
· 生成设计文档 |
0.5 |
0.5 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
0.5 |
0.5 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
0.2 |
0.2 |
· Design |
· 具体设计 |
1 |
2 |
· Coding |
· 具体编码 |
4 |
8 |
· Code Review |
· 代码复审 |
3 |
1 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
2 |
5 |
Reporting |
报告 |
|
|
· Test Report |
· 测试报告 |
1 |
1.5 |
· Size Measurement |
· 计算工作量 |
0.2 |
0.2 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
1 |
1 |
合计 |
14.9 |
21.4 |
这次写程序很坑的一点就是对C++语法和其内存回收机制的理解不深,而且由于VS2012编译器对数组越界不会报错的情况和自己编程不规范,又是漏写括号,造成了自己在具体编码时花费了不少时间在查资料和调试上。所幸这次自己在写代码前非常认真的理解和分析了项目需求,把要求的每一点都考虑到了,确立了正确具体的设计思路,使得在写代码的过程中思路一直没有变,一直是按照最初的设计去写,最后写完后也能很好地运行。并且有助于后期对代码的优化和性能的提高。
二.改进程序性能
我在改进程序方面大概用了3个小时,其中包括设计优化思路,改代码,程序调试。其中调试所占的时间比重最大。
程序的整体思路:通过随机生成前缀表达式来对应生成唯一的中缀表达式,并计算表达式的值。表达式的运算数,运算符,运算符的个数都是随机产生,按照字符串前面全是运算符,后面全是运算数来拼接产生前缀表达式,利用表达式的哈希值来将重复的表达式筛除,这其中一旦出现不符合的表达式就重新生成新的表达式。
我的优化思路主要有以下几点:
1.改进随机算法:随机算法生成四则运算最为耗费性能的一点就是生成了一个不符合规则表达式,然后再去重新生成。其实我们可以对那些不符合规则的表达式建立修改策略,使之成为合法的表达式,避免重新生成浪费时间。比如:对于计算过程出现负数的表达式,可以把其中的减号改成其它的运算符;对于出现除0的这种情况,随机产生一个正数将其替换;
2.对判重操作的优化:对于判重这一点,我一开始的想法是编写相应的函数用来去检测表达式之间是否重复,但这样的做法在空间和时间上耗费都很大。对此,我想到一点,我们直接就生成不会出现重复的表达式,从而避开繁杂的检查判重操作。我的做法是强制规定生成的表达式的运算符必须为降序排列,同时利用表达式的哈希值来去除完全相同的表达式,因为表达式中的运算数遵从有序性,所以程序不会同时产生3+2+1,1+3+2这两个表达式,只会生成3+2+1,这一点可以完全排除表达式重复的情况。
虽然这样的做法使得一些原本正确的表达式不能被生成,但是在测试中,r大小为3时,就完全可以生成10000道表达式,足以见得这样的做法是满足项目需求的。
3.在性能分析的过程中,当r的值增大(>30)时,计算表达式的值会非常耗费时间,主要是因为分子分母的位数过大导致,其中寻找最大公因数的函数gcd()占用了大量时间,所以着重优化了gcd()函数的代码结构。当n的值增大时(>10000),我发现最为占用时间的方面是将生成的表达式转换为string类型并输出的过程。一开始我使用的是stringstream来实现将int转换为string,但这种做法非常耗时,于是我自己重写了相关函数将int转换为字符串数组,虽然代码行数增加了,但是效率得到了提升。
可以发现当n和r的值不断增大时,程序中真正耗时的函数并非是用于生成随机表达式的核心代码,而是那些类型转换的相关代码,所以在优化代码时不应该仅仅考虑核心算法的优化,而且要考虑数据结构是否合理,便于运算输出等性能,这是性能分析给我的启示。
下附性能分析图(命令行输入为*.exe -n 10000 -r 10)
程序中消耗最大的函数
三.发现的bug
这里记录一下我发现的比较棘手的bug:
1.vs2012在C++编译过程中,对于数组越界不报错的问题:
这个问题我一早没有意识到,在整个程序写完进行调试时,程序运行正常但是运行结束时总会crash掉。后来意识到是数组越界的问题,但500行的代码去找数组越界的情况确实相当麻烦。
2.int整型溢出的问题:
由于分数的存在可能会导致运算过程中出现较大的分子分母,据我推算,当r的值为15时就有可能发生int整型溢出的情况。为此,我将int改为了unsigned _int64,可以支持到r的值200的情况。若r的值再大,程序就报警了!!
四.测试用例
1. -n 10 -r 1
程序输出错误提示信息,因为程序规定r的取值范围为(0,200]
2.-n 1000 -r 1
程序输出错误提示信息,因为取值范围为1可以生成的表达式数量小于1000
3.-n 20000 -r 10
程序输出20000道表达式,运算数范围为[0,10)
4.-n 1000 -r 100
程序输出1000道表达式,运算数范围为[0,100)
5.-n 10000 -r 100
程序输出10000道表达式,运算数范围[0,100)
6.-n 1000 -r 300
r的范围超过程序设定的上界,程序输出错误信息
7.-e Exercises.txt -a Answers.txt
测试的txt中没有错误答案
8.-e Exercises.txt -a Answers.txt
测试的txtx中有10个错误答案
9.-e Exercises.txt -a Answers.txt
对应的txt不存在,程序输出提示信息“please input correct parameters!!”
10.-e Exercises.txt -a Answers.txt
两个txt长度不匹配,程序将按短的txt文件进行判错
程序正确性证明:
主要算法:生成前缀序列,将其转为对应的中缀序列表达式。生成的前缀表达式遵从这样的规则前缀表达式运算符都在前面,操作数都在前面,生成的操作数按照降序排列且生成的操作数不能完全一样。从而保证了不会出现交换重复的表达式,因为同一表达式发生交换重复,运算数的有序性必然被打破,这在程序中是不会出现的。另外要储存每个表达式的哈希值,通过判断哈希值来去除相同的表达式。这样保证了程序不会出现交换重复或生成重复的表达式。
五.个人总结
1.首次实践了软件项目开发所需的各个阶段,虽然感觉和平时写程序没什么区别,但是设计需求分析阶段确实是十分重要,避免了代码重复,思路中途更改等问题。
2.初步掌握C++的相关语法知识,包括常用函数的使用,如stringstream,ofstream,ifstream,map等,而且对C++内存管理和回收机制有了一定了解。第一次在VS2012上进行编程,虽然有很多坑,但是熟悉之后感觉还算可以,而且其性能分析工具对提升程序性能和程序性能优化确实有很大帮助。
3.改正了自己的优化习惯,对于程序优化我习惯上是针对核心算法进行优化,比如这次我的主要精力就是优化随机生成前缀表达式算法,但是在后来的性能分析中我发现核心代码占用的时间不到1%,真正占用速度的是数字到字符串的转换过程。而这主要是因为自己设计的分数这个数据结构有问题,使程序输出是还要多转换一次,将分数这一数据结构进行了一定的修改,程序的速度提高了一些。我觉得这纠正了自己在优化方面误区,对数据结构的优化也是很重要的。