浅谈程序的核心——复杂度
在《The art of unix programming》中,复杂度的控制被视为编程项目的核心要点,其中提到编程项目的核心就是对复杂度的控制,而simple原则也围绕着这一核心展开。
我在2008年曾撰写过关于“复杂度与习惯”的文章。7年过去,经历了《天涯明月刀》这类大型项目的磨砺,我对复杂度有了更深刻的认识。
复杂度的要点
复杂度的关键在于程序给开发者大脑带来的负担,它直接反映了程序员提升和开发程序的难易程度。这种负担会随着模块复杂度的增加呈平方级数增长。
当程序带来的负担较低时,程序员就能更轻松地控制程序,进而提升程序的质量,包括开发效率、运行稳定性和运行效率。不过,我们并非在所有情况下都要追求复杂度的最小化。如果一个模块本身规模较小,就无需投入过多精力进行进一步简化。当然,若时间允许,出于自我提升和精益求精的目的,进行简化也是有益的。
需要强调的是,低复杂度并不等同于代码行数最少,而是指给大脑带来最少负担的代码。例如后文所举的代码示例,尽管另一种写法代码行数更多,但由于它遵循更稳定的模式,在大脑负担和心理负担方面都更轻,因此可被视为复杂度更低的代码。
复杂度控制的实际意义
实际价值
从实用角度来看,复杂度控制关乎程序的运行效率和开发效率,尽管扩展性等方面也受其影响,但在实际项目中,运行效率和开发效率的体现尤为明显。
7年前,我就认同复杂度控制的重要性,但在实践中才真正形成了有效的开发原则。
开发效率
以开发地形系统为例,该系统涵盖编辑器底层部分(UI部分由其他同事负责)和runtime部分,从材质到高度图,规模庞大且复杂。在开发过程中,不可避免地遇到了需求变动,如材质系统能力和地图大小等方面的重大调整。
由于时间紧迫、任务繁重,我原本计划先快速完成开发,后续再进行代码整理和系统整体控制,以便其他组能够同步开展工作。然而,对于如此庞大的系统,这种策略并不适用。
在编写程序时,若能始终对整个系统在代码层面保持清晰的认知,明确代码的结构和逻辑,编写过程就会十分顺畅,如同行云流水,让人享受其中。相反,如果在实现过程中缺乏对系统的良好认识和整理,采用“先完成再整理”的方式,在小型项目中或许可行,但在大型系统中,即便思维清晰,系统的混乱也会导致编程过程磕磕绊绊,令人疲惫不堪。
后来,我改变策略,先对代码进行充分整理,去除不必要的部分,使代码达到可进行新功能实现的状态,再开始编写新代码。这种方式反而提高了开发速度,让编程过程更加愉悦高效。
运行效率
在处理程序运行效率方面,常规做法是通过profile热点分析以及根据游戏情况关闭某些功能。但这些方法的作用有限,若要进一步提升性能,接近性能极限,必须做到以下两点:
- 对每一个模块有充分的理解。
- 能够快速进行反复尝试和迭代。
在优化早期,处理性能热点是一种高效的方法,它能在程序存在性能提升空间时,快速提高性能。然而,在追求极限性能时,热点优化就显得不够了。例如,某个模块的性能消耗是否超出合理范围,排名靠后的模块是否需要高频运行等问题,热点处理无法解决。
只有对程序有充分了解,才能进行更彻底的调整,如将大量运行任务并行处理、低频执行或直接优化掉。实践证明,这种处理方式能将程序性能提升到一个新的高度。
但要实现这一点并非易事,对于像《天涯明月刀》客户端这样的超大系统,包含几十万行代码,如何充分理解程序并进行彻底的修改优化是一大难题。因此,关键点又回到了复杂度控制上,只有将程序的复杂度控制在最佳状态,才能更好地完成性能优化工作。在实际优化过程中,大约一半的时间都用于代码的调整和重构,合理的代码结构能使优化更加可行和高效。
复杂度控制的方法与实践
实践表明,复杂度控制能力可从渴望、目标和时间积累三个方面进行提升。
渴望
最有效的方式是承担覆盖范围广泛的实际开发任务。在这种情况下,开发者会深刻体会到复杂度带来的困扰,从而真切认识到复杂度的本质,明确哪些因素至关重要,哪些则无关紧要。有了这种强烈的渴望,后续的积累和实践就会更加顺利。
目标
复杂度控制的方法和实践多种多样,但目标却很明确,即始终保持对整个系统在代码层面的清晰认知。在开发设计和决策过程中,能做到心如止水、顺畅无阻。在一定程度上,复杂度控制具有主观性,需要把握好分寸。例如,对于小型项目,即便复杂度控制不够理想,但如果系统依然清晰可控,就可以将更多精力投入到其他方面。
方法
在个人实践中,可从以下几个方面控制复杂度:
- 任务切分与代码整理:在小型任务完成后,及时进行小规模的代码整理,确保代码始终保持整洁。
- 模式积累:积累更多的编程模式和算法。例如,当看到一大片代码实现的是pool功能时,就可以将其视为一个整体,用“pool”来概括,从而减少需要关注和记忆的内容。通过不断总结和积累,在开发和思考时就能从更高的维度进行,有助于压缩复杂度,提升思维速度和质量。
以下是两个代码示例,展示了复杂度的差异:
// 复杂度较高的代码
int a[5];
for(int i = 0; i < 5; i++) {
printf("%d", a[5]);
}
// 复杂度较低的代码
#define ARRAY_NUM(a) (sizeof(a) / sizeof(a[0]))
int a[5];
for(int i = 0; i < ARRAY_NUM(a); i++) {
printf("%d", a[i]);
}
第一个代码示例中,需要注意数组越界的问题,而第二个代码示例通过宏定义减少了需要关注的内容,可视为复杂度较低的代码。
因此,保持总结积累非常重要,对编程模式和算法的积累越多,在开发过程中就能以更高的维度进行思考,从而有效压缩复杂度,提高思维速度和质量。同时,尽量采用返璞归真的编程风格,有助于更好地控制复杂度。
复杂度控制的“敌人”
未意识到“复杂度”的重要性
许多程序员(甚至大部分)对复杂度缺乏敏感度,将算法和效率因素的重要性置于复杂度之上,甚至以编写复杂程序为荣。这种观念使得沟通变得困难,只有亲身承担大量程序实现,深刻体会到复杂度带来的痛苦,才能真正认识到其重要性。
此外,部分程序员没有及时与项目组沟通,争取足够的时间来处理复杂度问题和清理代码。由于大部分程序员对复杂度缺乏充分认识,要求项目经理具备足够的认知并不现实。因此,程序员应加强沟通,并在实现估时上预留充分的余量。若忽视复杂度,盲目追求实现时间,将对项目造成不利影响。
进度问题
在实际项目中,时间紧、任务重的情况屡见不鲜。每个程序员都应建立代码实现的profile机制,如使用worklog记录开发过程,跟踪自己的开发效率,以便了解哪种方法更高效。根据具体情况采取合适的策略,在多数情况下,边实现边整理代码是更快捷的方式。同时,扎实的编程基本功是快速稳定实现代码的关键,这需要长期有意识的积累。
“good for the programmer’s soul”
John Carmack曾说:“Low-level programming is good for the programmer’s soul.” 我对此深表赞同。深入理解底层编程,掌握硬件和系统的工作原理,有助于程序员清晰地理解整个程序的运行机制。同样,在高层复杂度控制方面,通过积累优秀的编程实践,更好地理解系统需求,最终实现简洁高效的代码,能让开发者在编程过程中达到心如止水的境界,享受编程的乐趣。
综上所述,复杂度控制是程序开发的核心要素,只有充分认识到其重要性,并采用有效的方法进行控制,才能提高开发效率和程序性能,实现高质量的程序开发。