|
文/顾煜 专栏:https://zhuanlan.zhihu.com/gu-yu
前文回顾:The world at your fingertips — 天涯明月刀幕后11(首秀)
这个版本暴露出了一些问题,我们整理了一下,开始一一解决。为了第一次见玩家做准备。
一个是规模越来越大,开发迭代变慢了。光用ssd,已经不足以满足我们的胃口,我们还希望编译过程更快更好。
二是地图开发成为一个巨大的问题。传统的开发流程,已经很难支撑这个项目的规模,我们想做更大的地图,更多的内容。而且地图的质量也亟待提高。
三是性能也需要逐渐纳入考虑,技术评审亮了警告,在这个时刻,分明将开发过程分为两段。往前看,是预研,可以任性的随意开发,往后看,要见玩家,该收敛的特性,也需要逐渐收敛。
后面几回展开讨论这几个话题。
编译提速
正如影响策划、美术开发效率的是编辑器质量,影响程序开发迭代时间的一个重要因素是编译速度。项目规模略大,影响大家的迭代速度了,我想花点时间处理一下这个事情,提高一下编译速度,给团队加点迭代效率的Buff。
这又是个不在关键路径上的任务,不影响团队主体任务,便于用碎片时间开发和研究,搞定最好,失败也无妨的事情,比较适合我来做。
先研究Compile阶段。
这个阶段相对还好,可以用Incredibuild之类的分布式编译工具来提速。Incredibuild的基本原理,是在开发团队每个人的机器上都装一个Incredibuild的客户端,然后有一个Server来协调同步,把所有的Incredibuild客户端都登记起来。当有电脑需要编译的时候,所有的编译任务通过Incredibuild来进行,它的客户端会将编译单元分发到不同的客户端,共同编译,最后把编译结果汇总回来,在本机完成最后的Link。
我做了点试验,发现这里还是有巨大的坑。我做了详细的编译时间统计,看了使用或者不使用incredibuild。我注意到,整个工程中的多个项目,情况不一,有些能从Incredibuild中得到巨大的收益,有些不能,反而会更慢。这个就让人非常不能理解。
深入看了一下,做了点研究。Incredibuild的工作原理,是把每一个CPP的编译单元,发送到不同的机器上去编译,在我的理解里面,一个编译单元包括了一个需要编译的CPP,以及所有的Include的头文件,这是相当巨大的数据,大型项目错综复杂的Include关系,可以轻易让这个编译单元多到数十M。另一方面,大项目为了加速编译,都会有一个PCH预编译头文件,原理就是把这个项目中每一个CPP文件需要的公共头文件放在一起,一次编译生成一个PCH文件,供后续所有CPP编译的时候直接使用,避免重新编译这部分公共头文件。这本可以带来巨大的提升,可惜和Incredibuild合作的时候就不那么完美了。
Incredibuild为了让每个编译单元可以独立编译,要把这个预编译PCH文件发送到每一台机器上去,然后才能开始编译。一部分项目的预编译头文件比较小,这个过程很快就结束了。另一些项目比较大,这个传输PCH文件的过程,本身就会占用很长的时间,偏偏我们的开发用网络还是百兆网,更是雪上加霜,导致后续通过分布式编译节约下来的时间,无法弥补因为传输PCH文件带来的开销,反而就变得更慢了。
解决方法就不太好办了。要么我给IT写个邮件,鼓励他们把全公司的内网都升级成千兆网,但想想自己的影响力似乎也没有那么大,就放弃了这个念头。另一个思路是把预编译头文件拆开变小,PCH小了自然初始开销也小,虽然会给后续每一个编译单元带来更多的编译时间,但因为这个时间分散在很多不同的电脑上,所以综合看来还是可以赚的。我做了些尝试,把PCH拆散拆细,果然是有效的,就是太累了,需要手工调整很多项目。
仔细看看本机编译,其实多数时间里Incredibuild能够带来的收益也不算太大。高速CPU+SSD带来的吞吐量和计算量提升,很多时候能弥补Incredibuild带来的分布式编译提升。很多年前,CPU还不是那么快,硬盘还是HDD,那时候分布式编译的确有巨大的好处,但目前这个项目规模下,本机编译应该是更靠谱的一个选择。
值得一提的是,在不同规模的项目下,是否使用Incredibuild是一个需要经常重新评估的事情。天刀在那段时间的开发中,的确是本机编译更快,但在更后期的开发中,随着规模进一步变大,逐渐Incredibuild也显示了一定的优势,所以还需要综合考虑。当然如果有千兆网甚至万兆网的话,Incredibuild应该是一个最好的选择了。
看完Compiling,我再看Link时间。
Link时间其实是更影响日常开发效率的一个因素。对于大规模分布编译,一般只在项目需要完整才有价值,而程序员的日常开发,其实都是修改几个CPP文件就要编译一次。那个情况下,都是增量编译,Link时间才是影响迭代的大头。特别是对GPP程序员,经常需要做细微的调整,迭代速度会被Link时间影响。
Link时间的优化方法并不好找,之前也没有太多的积累。正好由于机缘巧合,看到一篇文章【1】很有意思。 文章提到了做了一个Python脚本,利用Dumpbin来扫描解析所有生成的Obj文件,从中检查有多少重复的导出函数,然后试图去掉这些函数。这样做后,Link程序要处理的导出函数更少,需要Resolve的函数少了自然就更快。这个思路非常棒,我也在我们项目中做了点实验。
照例先树立测试标杆。我挑选了一个编译最慢的项目,在什么优化都没做前,编译一次,用了9.2s,扫描后发现一共导出了36400个函数。
仔细检查了函数表,大量函数都是我们在生成对象描述元信息的时候,在宏定义里带入的。相当多的Inline函数,其实并不是为了提高性能,只是在写类定义的时候,觉得随手可以写到头文件里的,本身也没多少语句,那就顺手这么做了。既然这是一个影响效率的因素,那我就认真重新写一下生成对象信息的宏语句,只保留函数的申明,把具体函数分散到CPP中。
这个改动有一定的效果。Link效率有了一定的提高,快了0.7s,变成8.5s的Link时间,导出函数变少,只有25700个了。虽然进步不大,不过这个方法有效,也让我很高兴,没做过的事情,总是很有乐趣。
继续优化。这个项目是一个GPP项目,需要根据策划配置数据,生成大量技能表格,供服务器和客户端共享。但这又不是一个必须的模块,并不经常变化,我就把这个模块移到一个独立的项目,严格说这个事情并没有减少总共所需的编译量,但还是会有帮助,把经常变化的和不常变化的分开,那么多数时间我们就不用处理那些不常变化模块带来的编译时间开销。
又快了2s,Link时间顺利缩短到6.5s,只有13200个函数被导出了。
我们进一步努力,做一些同一数量级上的优化,移动任务表格、Buff表格等数据源到公共项目中,进一步缩短了Link时间,5.9s,10900个函数被导出。
最终看见剩下的导出函数大头,都是在Speedtree里面,Speedtree的头文件,被大家偷懒加入了公共的头文件,而事实上使用Speedtree函数的文件并不多。我毫不犹豫的从公共头文件中去掉了Speedtree的头文件,然后所有调用Speedtree函数的Cpp文件编译就出错了。我根据项目编译出错情况一一修正编译错误,在那些Cpp文件中,单独include speedtree的头文件。这样做完,我们就避免了把speedtree头文件中的inline函数在多个不同的地方被调用,也就顺利减少了导出函数。经此一役,导出函数大幅减少到700个,时间缩短了1.4s,只需要5.1秒就能Link完。
忙了1天多,终于把一个库的link时间降低了大约4s。后续我又用碎片时间,陆陆续续把其他一些link较慢的库也做了一些相关的调整。
Link时间的优化对整体效率有着隐性微妙的提升。提高4秒看似微不足道,但如果一个程序员每小时编译10次,一天就是80次,省320s,整个团队30程序,省9600s,已经是相当可观的一个时间。更不要说编译过程如果够快,迭代过程够顺利,有助于程序员更集中注意力,程序员也不至于在编译中开小差,团队程序员总在上网、上厕所、看手机往往是有原因的,说不定就是迭代太慢编译速度不够。
下次我们再说地图的方案,那是早期测试中的重要技术亮点。
【1】Speeding C++ links
|
|