作业 20180925-3 效能分析

  此作业的要求参见:https://edu.cnblogs.com/campus/nenu/2018fall/homework/2145

  该作业git地址为:https://git.coding.net/fuj905/wf.git

 

作业:对上周作业中的功能4 (仅由文件重定向读入,不由控制台读入) 做效能分析

 

 

要求0: 以 战争与和平 作为输入文件,重读向由文件系统读入。连续三次运行,给出每次消耗时间、CPU参数。

 

 使用命令行进入程序所在文件夹,输入如下命令:

ptime wf -s < war_and_peace.txt

 

  2018.10.5 16:52时连续测试三次运行时间截图为:

  

 

  消耗时间汇总:

第一次运行时间 0.952s
第二次运行时间 1.099s
第三次运行时间 0.966s
平均运行时间 1.006s

 

  CPU参数: Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz 3.50GHz

 

 要求1 给出你猜测程序的瓶颈。你认为优化会有最佳效果,或者在上周在此处做过优化

 

一、 上周做过优化的地方

  最初版本

text = text.replace('\r',' ').replace(' ',' ').replace('.',' ').replace(',',' ').replace('"',' ')

  修改版本

for ch in '\r .,"':
        text = text.replace(ch,' ')

  此处是删除文档中的冗余字符(非单词部分),将冗余字符替换为空格可以使用replace()方法,但是反复replace()会比较浪费时间,修改版本则是将所有的冗余字符封装在一个字符串里,进行一个for循环,降低时间复杂度。

 

二、 猜测的瓶颈

 1.  处理字符时遍历整个文档耗时较长

  字符处理代码:

def dispose_words(string):
	string = string.replace('\n',' ').replace(',',' ')
	s1 = list(string)
	num = len(s1)
	s1.append(' ')
	for i in range(num): # 遍历整个文档
		if s1[i] in '."?\')-(;#$%&*!':
			if str(s1[i-1].isalnum())=='True' and (str(s1[i+1].isalnum())== 'True'):
				pass
			else:
				s1[i]=' '
	for i in range(num):
		if s1[i] in ':':
			if s1[i+1]=='/':
				pass
			else:
				s1[i]=' '
	s = ''.join(s1)
	return s	

  此处为去除冗余字符及保留特殊字符(例如网址)共有两次使用for循环遍历整个文档,对于大文件而言,比较耗时。考虑到上述工作在词频统计任务中是必须的,因此可以设置一个for循环,预计会节省时间。

 

 2. 将所有单词分离并小写化时遍历整个文档耗时较长

  分离、小写化所有单词代码:

def countWordsFrequency(text,flag):
	text = dispose_words(text)
	list1 = text.replace('\n',' ').lower().split()# list1保存小写化后的原始数据

  此处考虑到句首的单词首字母会大写,为了统计相同的单词所以需要将所有单词分离并均转化为小写,故需要进行一个全文遍历后转为小写。lower()函数与split()函数需要进行一个全文遍历。但考虑到功能性,此处的遍历是必不可少的。

 

 

要求2 通过profile 找出程序的瓶颈。给出程序运行中最花费时间的3个函数或代码片段。

  

  由于本程序作业使用Python语言,故在查阅资料之后使用cProfile进行效能分析。

  效能分析步骤:

  0. 使用命令行进入程序所在目录后输入以下命令:

python -m cProfile wf.py war_and_peace.txt

  1.因为上述命令行得出的结果不够直观,故采用下述命令行进行分析:

python -m cProfile -s time wf.py -s < war_and_peace.txt

  2. 得到分析结果如下:

  

  其中:

     

    ncalls:表示函数调用的次数;

    tottime:表示指定函数的总的运行时间,除掉函数中调用子函数的运行时间;

    percall:(第一个percall)等于 tottime/ncalls;

    cumtime:表示该函数及其所有子函数的调用运行的时间,即函数开始调用到返回的时间;

    percall:(第二个percall)即函数运行一次的平均时间,等于 cumtime/ncalls;

    filename:lineno(function):每个函数调用的具体信息;

   

  由测试结果可知:我的程序最耗时的函数为“dispose_words”(去除冗余字符),“countWordsFrequency”(统计词频)以及main函数,最耗时的代码段为“dispose_words”函数中的“split()”、“join()”方法。

  其中耗时前三为:

    1. “dispose_words”函数:调用1次,运行时间为0.442s。

    2. “countWordsFrequency”函数:调用1次,运行时间为0.765s。

    3. “split()”方法:调用1次,运行时间为0.047s。

  代码如下:

  1. “dispose_words”函数

  #功能0:去除冗余字符
  def dispose_words(string):
	string = string.replace('\n',' ').replace(',',' ')
	s1 = list(string)
	num = len(s1)
	s1.append(' ')
	for i in range(num):
		if s1[i] in '."?\')-(;#$%&*!':
			if str(s1[i-1].isalnum())=='True' and (str(s1[i+1].isalnum())== 'True'):
				pass
			else:
				s1[i]=' '
		if s1[i] in ':':
			if s1[i+1]=='/':
				pass
			else:
				s1[i]=' '
	s = ''.join(s1)
	return s	

  

  2. “countWordsFrequency”函数

  # 功能1:统计词频。	
  def countWordsFrequency(text,flag):
	text = dispose_words(text)
	list1 = text.replace('\n',' ').lower().split()# 保存原始数据
	list2 = list(set(list1) )  # 去重之后的数据
	if(flag == 0):
		print("total  " + str(len(list2)))# 小文本统计词汇量(功能1不输出words)
	else:
		print("total  " + str(len(list2))+ "  words")# 统计词汇量
	print("\n")
	dir1 = {} #计算频数
	for str1 in list1:
		if str1 != ' ':
			if str1 in dir1.keys():
				dir1[str1] = dir1[str1] + 1
			else:
				dir1[str1] = 1
	dir2 = sorted((dir1).items(),key = lambda x:x[1],reverse = True) # 按照频数排序
	if (len(dir2) > 30):
		count = 10
	else:
		count = len(dir2)
	for x in range(0,count):
		print('%-10s %-10s' % (dir2[x][0],dir2[x][1])) #美化输出频数

  

  3. “split()”方法

  def countWordsFrequency(text,flag):
	text = dispose_words(text)
	list1 = text.replace('\n',' ').lower().split()# 保存原始数据

  

要求3 根据瓶颈,“尽力而为”地优化程序性能。

 

     由cProfile分析结果可知:程序耗时最久的为“dispose_words”函数中的“split()”方法、“join()”方法、“lower()”方法、“replace()”方法、“isalnum()”方法,以及“countWordsFrequency”函数中的“keys()”方法。

    考虑到上述功能在整个词频统计功能实现中无法避免的要进行遍历,结合要求1中的猜测,可以将“dispose_words”函数中的“isalnum()”方法与针对“/”的for循环放在一起,即只进行一次遍历。

    修改代码如下:

    for i in range(num):
		if s1[i] in '."?\')-(;#$%&*!':
			if str(s1[i-1].isalnum())=='True' and (str(s1[i+1].isalnum())== 'True'):
				pass
			else:
				s1[i]=' '
		elif s1[i] in ':':
			if s1[i+1]=='/':
				pass
			else:
				s1[i]=' '
	s = ''.join(s1)

  

要求4 再次profile,给出在要求1 中的最花费时间的3个函数此时的花费。

   

  修改之后再次cProfile,测试结果见下图:

  

  对比优化之前可知:

  (1) “dispose_words”函数:依旧调用1次,运行时间由0.442s下降为0.395s。

  (2)  “countWordsFrequency”函数:依旧调用1次,运行时间由0.765s下降为0.720s。(此处虽未直接修改 “countWordsFrequency”函数,但其执行时会先调用 “dispose_words”函数,故运行时间会有所下降)

  (3) “split()”方法:依旧调用1次,运行时间由0.047s下降为0.046s。

 

  再次profile,2018.10.05 21:08连续测试三次运行时间(对应于要求0)结果见下图:

  

  

  

  

  消耗时间汇总:

第一次运行时间 0.902s
第二次运行时间 0.893s
第三次运行时间 0.890s
平均运行时间 0.895s

 

  CPU参数: Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz 3.50GHz

 

   对比可知:平均运行时间由1.006s下降为0.895s

 

  

要求5 程序运行时间。

  等待教师的测评。

 

        

 

posted @ 2018-10-05 21:21  Flora1014444  阅读(199)  评论(0编辑  收藏  举报