这些年,你写了多少行代码


又到了辞旧迎新的时候。最近已经被各路大拿的总结砸的晕头转向了,心想人家的2016为何能做那么多事那么精彩呢?不过还好,我有“幸存者偏差”这一法宝安慰自己……

言归正传,前一阵突然想到,自己进入项目组已经差不多两年了,不如用最朴素的方式——统计代码行数——来总结下吧。

虽然目前已经有各种各样的工具可以做这件事情,不过自己写一个应该也很快,说不定还有其他发现呢。

初版


统计“现存”代码行数,无非就是找到所有需要统计的文件,svn/git blame,然后一行一行的分析计数即可。所以第一版很快就写出来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ignore_paths = ['ignore/path1', 'ignore/path2']
code_stats = defaultdict(int)
for root, dirs, files in os.walk('/path/to/codes/root/'):
ignore = False
for ig_path in ignore_paths:
if ig_path in root:
ignore = True
if ignore:
continue
for fname in files:
if not fname.endswith('.py'):
continue
abs_file_path = join(root, fname)
stat_file(abs_file_path)
print code_stats

其中,ignore_paths中存储的是一些机器生成代码的目录,我们将其排除在外,可以节省一定的时间。

stat_file函数具体处理统计每一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmd_prefix = 'svn blame '
def stat_file(fname):
cmd = cmd_prefix + fname
args = shlex.split(cmd)
with open('blame.py', 'w') as f:
subprocess.call(args, stdout=f)

with open('blame.py', 'r') as f:
for line in f:
args = line.split()
if len(args) >= 2:
author = args[1]
if '@' in author:
author = author.split('@')[0]
code_stats[author] += 1

这里我们利用了svn blame得到的结果格式,获得每一行代码的当前作者信息。

整个程序很简单明了,但是运行起来,太太太太慢了……首先所有文件的统计技术都是串行的,其次svn blame的运行本来就很耗时。但是先不管这么多,运行完看看结果先……

于是我去做其他事情了。不知过了多久,终于统计出结果了~结果先暂时保密……

改进版


接下来就开始考虑改进执行效率。对于这种有阻塞IO、各个任务之间又基本互不干涉的情况,当然优先考虑多进程/多线程的方式了。不过究竟用多进程还是多线程呢?这里有个神器multiprocessing.dummy。multiprocessing大家应该都熟,是Python中的多进程模块,而这个dummy呢,则是实现了multiprocessing的所有接口、模块,不过内部是按照多线程的方式实现的。

这样的话,我们可以先按照多进程multiprocessing的方式写程序,之后直接将multiprocessing替换成dummy,就可以得到一个多线程版本,是不是炒鸡方便?

多线程/多进程程序,首先要对所有的任务进行划分,我们这里策略比较直接:将所有文件按照进程数P_NUM切分成P_NUM份。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  
P_NUM = 8
def stat_files():
all_files = []
for root, dirs, files in os.walk('/path/to/codes/root/'):
ignore = False
for ig_path in ignore_paths:
if ig_path in root:
ignore = True
if ignore:
continue
for fname in files:
if not fname.endswith('.py'):
continue
abs_file_path = join(root, fname)
all_files.append(abs_file_path)

file_sections = []
file_total_nums = len(all_files)
for i in xrange(P_NUM):
start = i * file_total_nums / P_NUM
stop = start + file_total_nums / P_NUM
if i == P_NUM - 1:
stop = -1
file_sections.append(all_files[start : stop])

之后,我们使用Multiprocessing.Queue来存贮各个子进程的执行结果,最后进行汇总。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  
def stat_files():
###above###
all_files = []
res_queue = Queue()
processes = []
for section in file_sections:
p = Process(target=stat_file, args=(section, res_queue))
p.start()
processes.append(p)

for p in processes:
p.join()

total_stats = defaultdict(int)
while not res_queue.empty():
stat = res_queue.get()
for author, cnt in stat.iteritems():
total_stats[author] += cnt

print total_stats

每一个进程上的统计任务stat_file,除了需哟将结果放入Queue中之外,没有其他区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  
cmd_prefix = 'svn blame '
def stat_file(fnames, res_queue):
code_stats = defaultdict(int)
for fname in fnames:
print 'processing ', fname
cmd = cmd_prefix + fname
args = shlex.split(cmd)
blame_file_name = 'blame_' + os.path.basename(fname) + str(random.randint(0, 100)) + '.py'
with open(blame_file_name, 'w') as f:
subprocess.call(args, stdout=f)

with open(blame_file_name, 'r') as f:
for line in f:
args = line.split()
if len(args) >= 2:
author = args[1]
if '@' in author:
author = author.split('@')[0]
code_stats[author] += 1
os.remove(blame_file_name)

res_queue.put(code_stats)

那么,通过切换如下两种import语句,即可切换多进程/多线程:

1
2
3
  
from multiprocessing import Process, Queue
#from multiprocessing.dummy import Process, Queue

多于多进程和多线程到底哪个更快,不妨先分析一下:

首先,在这个小程序中,有很大的消耗在等待svn blame的返回结果,这里IO是瓶颈,那么多线程虽然有GIL,但是也可以实现并发。并且由于线程创建时的消耗小于多进程,所以在这一部分占得优势;

其次,得到svn blame的结果之后,会完整的分析整个文件,进行一些字符串的操作。这里是CPU密集的,而多进程可以做到真正的并行,所以多进程在这一部分具有优势。

再次,多线程的程序在svn blame阻塞时,会切换到其他线程执行。而多进程程序是每个核心各自执行,无序调度切换。这一部分,多进程占优。

那么最终的结果,就要取决于这几部分哪一部分所占的比重更大了。

还是直接看测试结果吧。我去除了所有不必要的中间print语句和sleep语句,最终得到结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
  
多进程:
real 0m36.350s
user 0m17.136s
sys 0m8.172s
多线程:
real 0m44.212s
user 0m17.824s
sys 0m8.716s
单线程:
real 4m44.543s
user 0m17.680s
sys 0m8.492s

从realtime看,多进程要快于多线程,大约是单线程的八分之一时间。从CPU使用率上看(user+sys/real),多进程版本的效率也更高一些。

结果


好,经过统计,大约2年的时间,在项目主体中我的存量代码大概不到4w行。按照平均每年200个工作日算,平均一天100行代码。不算高产,但也还算状态健康。当然,平时的状态能这么均匀就太好了!实际情况是,一天撸3000行也是有的,一周只挤出100行也是有的……

新年希望自己仍然能够保持这样的输出节奏,这样就有更多的时间,拓展自己的视野了~

那写这篇文章呢,并不是要以代码量衡量一个程序员的产出(明显是不靠谱的方式);而是想记录下,我通过做这件小事,理解了multiprocessing.dummy到底是个什么鬼,以及在出结果之前,如何理性的分析下多进程/多线程的优劣。希望对读者也有帮助。

相关代码可以在GitHub上找到code-stats

转载请注明出处: http://blog.guoyb.com/2017/01/09/stat-codes/

欢迎使用微信扫描下方二维码,关注我的微信公众号TechTalking,技术·生活·思考:
后端技术小黑屋

评论