|

楼主 |
发表于 2015-2-5 16:44:04
|
显示全部楼层
1.C#代码优化方案
本帖最后由 kushinn 于 2015-2-6 13:39 编辑
注: 本篇章所说的优化仅仅在于编译器优化之前的工作,对于编译器处理后的代码,有可能与这里提及的部分优化是一样的结果。 所谓优化,在我看来,大部分程序所能够接触到的,无非是可读性,以及逻辑复杂度,以及内存使用以及性能等优化。尤其是使用Unity3D这种引擎来说,因为我们都是使用者,无法去更改一些底层的流程、结构、渲染策略等等。
于是在这里我把目前遇到的一些难题,以及解决方案记录下来,以帮助后来人。同时,为了不误导他人以及提升自身水平,也欢迎行业人士对本人所述观点进行一定批评指正。只要不是无端谩骂,一切都是欢迎至极的。
一、可读性的优化
何为可读性,我相信有编程经验的人都会为之不屑,认为这种东西实在是难以把控,又或者是找了一堆的理由去劝说自己,以后会改正,相信我,也请相信你们自己,你们不会再有时间或者是愿意去改的。对于我们大部分紧急的需求而已,我们往往会很容易陷进一个Magic Number的误区。常常会写出一些莫说他人,连自己,过几个小时都可能忘记的代码。 if (go.tag == 12345) then .... tag = 12345 是什么,也许你当前是知道的,并且你也认为其他地方不会用到这个magic number,就算用到的时候,再去修改,然而,当维护者(另一个人)看见你这里是直接使用的时候,大部分情况下,他也会使用同样的方法去做这样的事情,这时,又有另一个维护者(人员变动对于IT行业而言,是很平常的事情)去维护,如果他想要修改这个tag的数值,他就需要到两个地方去修改,但是,如果他没有足够的时间去看所有的代码,而这两处代码又是极度分散的(暴露在外的),他也许根本就找不到其他地方的12345(Ctr-Shif-F 可能找到很多的调用,而这种常量无法通过IDE找到引用的地方)。这时候,很好,出了一个包(打包是一件缓慢的事情),测试,不行,然后又要回去修改。
对于这种Magic number的问题,我相信大部分有其他编程经验的人而言,都知道如何解决。那就是宏(C/C++/Objective-C等)或者是const或者是static readonly(C#)的方式去解决。
const int kMagicNumTag = 12345;
......//others
if (go.tag == kMagicNumTag ) then .... 建议1:无论何时,都不要为自己懒得命名一个magic number找理由,找一个合适的地方,放上const或者static readonly, 并且给它一个符合你的团队约定规范的命名。
除此之外,我们还有可能遇到下面的情形:
if (num == 1) {
doSomething();
}
我相信,明眼人一下子就看出问题所在。不错,那就是代码对齐问题。不管在哪个公司,总会有一些人喜欢随便的用空格啪啪啪打几下,然后容易出现这种风格的代码。然后理由肯定是,这些都是时间非常急导致的,实际上是怎么回事,大家一清二楚,就是因为没有养成这种习惯。其实这样的代码,十分影响阅读性,代码行数少,逻辑复杂度低还好,多套几个循环或者回调,就很少有人清楚了(包括维护者自身也不会清楚)。任何人总会喜欢找理由为这种代码找理由,这是要欺负我们这些代码整洁度的强迫症患者么?
建议2:无论何时,都不要为自己懒得去整理代码找理由,如果懒,那么请养成良好的编程习惯,并且为你的团队建立一个良好的编程规范。。懒在程序员角度来说是一种美德,但那并不是意味着是这个方面。
可读性的优化十分多,包括下文要提及的各种优化方面,由于篇章有限(40000个字节),所以这些经验之谈,就尽量少说,毕竟下文的Unity3D性能分析以及优化才是亮点。
二、逻辑复杂度
逻辑复杂度,其实大部分情况都与游戏本身的设计有关,这里也仅仅是提及了一下如何去避免一些由于代码扩展或者是滥用回调造成的问题。尤其是switch或者是回调嵌回调的写法,很多情况下,是一个无底洞。个人看来,数组或者容器,便是对于代码量极大大switch的最佳替代品。虽说性能上不一定比得上switch,但是,可读性上,以及整体的美观,是后者所不足的。
例如:
public static void DoSomethingByTag(int tag) {
switch (tag) {
case kTag0 : {
}
break;
case kTag1 : {
}
break;
//case ....
case kTag1000 : {
}
break;
default: {
}
}
}
public static void Run(Action callback) {
Action ac = () => {
DoSomething( ()=> DoSomethingByTag(kSomeTag));
callback();
}
//others
if (something) {
ac();
} else {
DoOtherAction(ac);
}
}
面对这样的代码,大部分人都是懒得去看,1001个case的代码,说实话,我是看不下去了,就算是卡神的代码,我也会跳过不看。很明显,这种代码如果出现,不是前期的设计(无设计)有问题,就是后期需求无抑制的扩展导致的,而不断去扩展的人,要么没有精力,要么没有能力去维护。试想,如果这些case打平处理,变成同一层次的子类或者是数组元素的概念,会不会更好理解以及更好扩展呢?
private static readonly Action[] TagDelegateArray = {
() => {},//tag0...
};
public static void DoSomethingByTag(int tag) {
//todo : verify here
//launch target action
TagDelegateArray[tag]();
}
//redesign!!
public static void Run(Action callback) {
}
建议3:为你的设计找一个平衡点,不要滥用switch(包括数组容器或者子类设计)导致复杂度的上升,同时,减少回调嵌入回调等代码的可能,滥用回调,有可能导致设计框架崩溃,需要重新设计或者项目崩盘。
三、内存使用以及性能等优化
在当前的移动设备而言,这点其实很重要,因为每一次的GC,都会导致本帧渲染耗费时间以几何倍数形式上升。所以一般情况下,我们不应该手动GC,GC并没有想象中那般智能,它如果进行回收,不会因为你是移动设备,就每隔一定时间进行一次处理。而foreach之类的代码,会为你隐蔽的进行许多小内存分配,别觉得它小,就没有关系,这在移动设备上,却是有可能极其影响你的程序表现的东西。为什么有时候你会觉得突然间卡住,然后又好了?这也许就是其中之一的原因——不可控的GC。在《Effective C#》一书中,推荐使用foreach代替for,但是,个人认为,这仅仅是基于可读性以及PC端的大内存的情况进行假定的,foreach会在生成迭代器IEnumerator的时候,分配一些托管资源,然后这些托管资源,会在合适的时机进行释放,然而,这个所谓的合适的时机,与Objective-C的autorealeasepool机制是否一样,没有仔细研究,但是如果你熟悉Objective-C,你应该清楚autorealeasepool的缺陷,当你这个pool包括的分配越大,最后同时一瞬间释放的也就越多,何况C#的性能与Objective-C的性能相比,就不用多说了吧。而string += 会造成的问题,自然是很明显了,如果连续+=大数据字符串(一般应该避免这种情况的发生,或者采用读写中间文件的形式进行处理),你会发现内存被吃的很快。
建议4:在移动设备上,在基于MONO的.Net版本的实现上,尽量使用for代替foreach。如果要进行字符串拼接处理,使用StringBuilder(参数不定),或者是string.format(参数数量已定)代替string的+=操作。
建议5:如果你的一次运算量过大,并且不要求在当前帧完成,请分帧进行处理,对每一帧进行迭代运算,不要把所有运算放在同一帧(例如,寻路算法,遗传算法等等)。
建议6:使用尾调用(弹跳床)等等实现返回,而不是使用返回中间变量的方式。
待续,篇章有点长,都是边想边写的,为了说的通俗,有些文字需要仔细斟酌一下....顺便吐槽,下次不在gameres发这种帖子了,一次就怕,代码根本没发敲啊,太不友善了
|
|