首页 > 技术文章 > 你最好对代码效率持有洁癖

xiayong123 2013-07-25 09:09 原文

“癖”这个词看着像个贬义词,而“洁癖”这个词在本人看来几乎就是个贬义词。虽然我每天跟一群不修边幅的屌丝程序猿打交道,但是对个人卫生还是相当讲究的,不过还远未到达洁癖的程度,我对洁癖的人多少有点鄙视,但是鄙视归鄙视,不能因为鄙视冲昏了头脑,我们不能否认有各种“癖”的人会在某方面做到极致,比如洁癖者的居住环境一定非常干净整洁,这是必须要肯定的,因为干净不止是视觉上的享受,对健康也是非常有好处。

虽然不能在讲究卫生上做到洁癖,但是能在程序效率上尽量朝着洁癖的方向发展也是一件偶尔颇具成就感的事情;关于程序的好坏大致可以从可读性和效率两方面评价(吧),当然以效率为更重要,功能性第一视觉性第二嘛,所以本着洁癖的态度本帖讨论程序效率上的几个问题。




1.       关于为了提执行效率而在函数内使用临时变量但是又会带来导致垃圾增多进而内存占用提升问题的讨论


怎么样,这个标题是不是非常像政府公文??并且还不容易看懂!

当一个函数内对某个非简单变量进行多次的引用时,我们通常的做法是建立一个缓存变量,来提高效率,简化代码,这一点是毋庸置疑要推荐的。那么是不是缓存变量一用就灵没有任何副作用呢,这个恐怕没那么简单!

首先我们举一个普通的例子,下面这个函数是我们对一个传入的数组中介于20到30之间的数字求和并返回。


private function getResult(list:Array):int

{

                    var result:int = 0;

                    var len:int = list.length;

                    for (var i:int = 0; i < len; i++)

                    {

                             if (isNaN(int(list))) continue;

                             if (int(list) >= 20 && int(list) < 30)

                             {

                                       result += int(list);

                             }

                    }


                    return result;

           }


看到这个函数可能有些同学已经忍不住要吐槽,因为int(list)这个变量在一个循环中出现了4次,首先int (list)本身意味着两次运算,list数组要到i位置进行一次引用值并返回,其次int()又要进行一次强转运算。本着吓死菜鸟的原则,我要分析一下这样写的后果:假如数组很长的话(比如几万个),那这个函数一旦运行就会有几万乘以4的运算,CPU利用率可能会瞬间飚上去,假如你开发手机应用那么配置低的手机可能会瞬间卡住,手机发烫,重启……假如很幸运的是该函数会被多次(上万次)调用,那后果就更严重,可能游戏运行到此处手机直接爆炸(可以用代码做炸弹了),所以int(List)必须被用临时变量缓存,提高效率,这点毋庸置疑!所以我们看第二个写法:



private function getResult2(list:Array):int

           {

                    var result:int = 0;

                    var len:int = list.length;

                    for (var i:int = 0; i < len; i++)

                    {

                             var mNum:int = list;

                             if (isNaN(mNum)) continue;

                             if (mNum >= 20 && mNum < 30)

                             {

                                       result += mNum;

                             }

                    }


                    return result;

           }


    看到这个写法,可能有些同学稍稍平静了,因为可能他们也是这么写的,的确,这样写理论上效率高了至少3倍。但是细心的同学会发现,在循环中,每次都创建了一个变量mNum,而它一旦被累加用完后就一脚踢开变成了垃圾,下次再创建一个,我们知道每定义一个变量,都要内存分配资源,而使用完毕后并不能立刻回收,这势必造成缓存垃圾占用内存,继续上面的假设,假如这个数组非常的长(几十万个),那么mNum就会被创建几十万个,函数执行完后他们都在内存里游离,如果幸运的是函数被执行了多次,那么就可能造成内存泄露,所以本着重复利用节约资源建设美好家园的原则,我们需要将这个缓存变量只在循环开始定义一次即可,这点也毋庸置疑是较优方案,所以我们有了下面的函数:



           private function getResult3(list:Array):int

           {

                    var result:int = 0;

                    var mNum:int;

                    var len:int = list.length;

                    for (var i:int = 0; i < len; i++)

                    {

                             mNum = list;

                             if (isNaN(mNum)) continue;

                             if (mNum >= 20 && mNum < 30)

                             {

                                       result += mNum;

                             }

                    }

                    return result;

            }


    看到这个函数想必某些老手也欣慰地点头了,因为他们可能也经常这样写,本人也经常这样写,并且认为这样写在CPU的利用和内存的节省方面,已经是一个比较好的方案。但是,如果你认定这就是结论,那么可能你还缺乏一点点远见,别忘了我们上面的假设:假如这个函数被调用了多次,多次是几次,谁也不知道,如果是几十万次的话,意味着内存中还是缓存了大量的垃圾,来自变量result、mNum、len、i。每调用一次,这四个变量便被定义一次,用外后直接无情地被抛弃。所以当你意识到或者已经被告知该函数会被大量重复使用时,你需要将这四个变量定义成类属性而不是局部变量,这样不管再多次调用,函数数组再长,这四个变量仅仅定义了四个(通常情况下),执行完后垃圾极少,所以我们又有了下面的函数:


           private var result:int = 0;

           private var mNum:int;

           private var len:int

           private function getResult4(list:Array):int

           {

                    len = list.length;

                    for (var i:int = 0; i < len; i++)

                    {

                             mNum = list;

                             if (isNaN(mNum)) continue;

                             if (mNum >= 20 && mNum < 30)

                             {

                                       result += mNum;

                             }

                    }

                    return result;

           }


    说句良心话,理论上单纯从执行这个函数的效率和对内存的占用来看,这是最佳方案!可是,可是除了运动就没有绝对的事情,诸位是不是觉得这种写法不是那么常见,虽然你不知道为什么但是你可能不经常这样写,那么将本该是函数局部变量定义成一个类的全局变量有什么问题呢?问题就是万一这个类会被大量创建对象使用时,刚才那些全局属性势必也会变成缓存垃圾,那么本着节省内存的角度考虑,被千夫鄙视的第一种方法仅仅定义了三个缓存变量,是不是又变得可取了?!不过这不是重点,重点是将局部变量变成全局变量还有另外一个更重要问题,关于变量初始化的问题,该问题我们放在最后一条讨论,防止有人看到这里就关闭了页面。

综上所述,缓存变量是推荐使用的,如何使用,看取决于函数的使用频率和你个人对效率的追求。




2.       关于一个函数内非满足条件时return的位置问题而引发的性能上的差异的讨论


    这个标题同样像政府公文一样除了当事人外别人看了有点不知所云,这就是让你往下看的原因。

    当一个参数传入函数内后,可能在某个地方因为不满足某个条件就直接被return了。这是一个too simple sometimes naive的工作,似乎没什么值得讨论,其实不然,因为return在什么地方被使用,差别是很大的。


举例如下:


一.检测鼠标

某个函数大概是这样写的。

private function getItmeDis():int

                   {

                            var dx:Number = stage.mouseX;

                            var dy:Number = stage.mouseY;

                            var dis:Number = 0;

                            if (showItem == null) return -1;

                            var cx:Number = dx - showItem.x;

                            var cy:Number = dy - showItem.y;

                            dis = Math.sqrt(cx * cx + cy * cy);

                            return dis;

                   }


这个函数的功能很简单,就是利用勾股定理计算鼠标跟showItem这个元件的直线距离,然后返回结果。其中有一个return -1,就是当showItem不存在时返回距离为-1这个特殊值。似乎没什么问题,但是这个函数在实际项目中表现非常不好,因为实际项目的使用情况是,这个判断是利用MouseMove事件来判断的,也就是说,鼠标一旦动起来,它就被连续不断地执行,而且showItem这个元件经常没有,这种情况下呢就经常执行return -1这句,每次return时都执行了两次访问鼠标属性,定义了三个空变量,都是无用的运算和无用的垃圾。所以正确的写法想必有人一开始就知道,那就是将if (showItem == null) return -1;放到函数的第一行,尽可能减少垃圾和对CPU的浪费,尤其是做移动开发的同学(!)。



二.设置变量时最好要判断一下当前值来防止重复工作造成的性能和资源浪费


    在一个项目中,set方法非常常用,随处可见。我曾见过一个方法这样写:

public function set uiWidth(value:Number):void

                   {

                            _uiWidth = value;

                            reDraw();

                   }

         功能非常简单,就是设置该对象的_uiWidth属性,并且来一次重绘。这样写完全没有问题,问题依旧出现在它实际的载体对象上,这个对象是一个按钮控件,大家都知道理论上控件在项目中的使用范围和频率是很高的,实际上呢这个控件在项目中大量地被使用,而这个控件功能比较强大,每次更新宽度都会带来皮肤更改,内部元件的位置调整,热区的更改,以及元件在舞台位置的判断等等,reDraw就是将这些更改重新执行和判断一边,是一个运算量较大的函数,因为控件的使用频率很高,并且有些新手在保证万无一失的情况下随意设置uiWidth,造成按钮会动不动重绘消耗CPU,包括(这个是重点)原本宽度没有变化的情况下所做的重绘,所以这个函数我们优化只需要加一个判断,杜绝重复的无用的执行:  


   public function set uiWidth(value:Number):void

                   {

                            if (_uiWidth == value) return;

                            _uiWidth = value;

                            reDraw();

                   }

如果你平时非常注意这些细节,那就当是提个醒了。



3.       关于vector数据表的两种清空方法带来的性能和可读性提升的讨论


虽然我尽量让标题看上去难以理解,但是这条似乎很难办到

可能当vector在as3里出现的时候,你就被告知要使用vector弃array,因为vector效率高,而且这种建议不止是身边的人还有各大技术论坛的鼓噪。

效率方面我不否认,我今天要提的是一个小细节,关于vector的清空。清空方法本人一直沿用Array时代的splice方法:

list.splice(0, list.length);

这样一用就是好几年,直到有一天在一个破解的文件里看一堆乱七八糟代码时发现有人这样用:List.length = 0;

于是测试了一下效率然后小吃一惊,对一个10万级到百万级的vector表进行删除,List.length = 0;这种方法的效率是前者的4-10倍。

可能有人很不屑,觉得平时自己项目的数据量上千都很难,别说十万级了,说实话,在万级别效率几乎没差,1毫秒的差别可以忽略不计,但是我还有一个理由说服你用第二种方法,那就是:代码量少,写起来简单!



4.       其实很多时候不需要那么多的get和set方法推荐用pubic比较好


有同学不知道为什么使用get和set方法吗?其实两方面来说吧,对于使用者而言,直接像一个简单变量属性那样可读可写非常方便,对于模块内部而言,一方面利于封装防止使用者直接访问你的某些重要变量,另一方面可以在set和get函数里进行一系列的转换操作。

有了这么正当的理由,本人相当一段时间变成了一个get和set控,但凡有变量都要这样访问和设置,并且自我感觉那是相当滴不错。

直到后来我成熟了,像反思自己以往很多白痴行为一样反思这种写法时不觉得大惊失色。因为很多时候一个类内部的变量就是一个简单变量,设置后没有进行什么复杂的转换;其次,更为重要的是,即时进行了复杂转换,访问这个变量的人还是我自己并没有别人访问也没有别的类访问,也就是说这个变量就相当于是一个模块内部的私有变量,出不了这个模块,所以根本不存在安全性和封装的必要!

至此问题抽象为public属性和set、get属性的效率问题。答案是显而易见的,当然是public效率高,因为public只访问了一次便能拿到属性值或修改属性值,而set或get要访问两次。并且:get和set让你的代码行数增多了,文件大了,你开发的时间长了,同样的功能要花更多时间了,同样的年龄和性别,为何你的脊椎有问题了,别人的没问题;代码行数多了,可读性差了,来一个刚毕业的新人时被吓住了,很难看懂了,本来人品和态度很不错的就知难而退了,实习期都没过,多可悲;假如对方还是个漂亮妹子,那损失岂不是更大,假如那个妹子没走,将来成了你老婆也不一定啊…所以还是擦擦冷汗别往下想了,于是结论来了:

模块内变量多用public,少用get和set!



5.       不要动不动就对一个类的所有变量进行初始化尤其是定义时的初始化这样做没必要而且性能上弊大于利


首先我要摆出结论那就是定义变量时给一个初始化的值这种做法是不好的,不推荐,尽管可能你一直在这么做。

随便举一个例子,让我们看看一个类的开头是不是有人会这样写:

public class BadGovernment extends Rubbish

{

           private var amount:int = 0;

           private var list:Array = [9,5,2,3]

           private var item:*= null;

           private var item2:*= null;

           private var info:String = "我觉得以经济建设为中心害了整个社会";


           public function BadGovernment()

           {

                    //

            }

}


         首先要说的是这种方式很不好,其次没有被我说中的同学别得意,因为你可能放到构造函数里了,像下面这样:

public class BadGovernment extends Rubbish

         {

                   private var amount:int;

                   private var list:Array;

                   private var item:*;;

                   private var item2:*;;

                   private var info:String;;


                   public function MM()

                   {

                            amount = 0;

                            list = [9, 5, 2, 3];

                            item = null;

                            item2 = null;

                            info = "我觉得以经济建设为中心害了整个社会";

                   }

       }


    如果这样写了,与上一条同罪!

    恰好逃离了这两条的同学也别开心的太早,因为有相当部分同学根本不知道到底该不该初始化,他自己都浑浑噩噩。

    为什么鸡蛋里挑骨头一样将这个行为挑出来说呢,因为曾经有一段真实的事情发生着本人身上,至今想起来都刻骨铭心。

    记得那还是多年前的2011年,我当时满脑子充满某种一夜暴富的幻想(就像现在的很多开发者一样)用flash做一个小游戏放到手机里去,那个时候智能机的还没有多年后的今天这么强大,所以虽然我满脑子幻想,但是在写每一行代码是还是很冷静地尽量保证了效率方面没有什么大问题,我知道手机的cpu跟pc机的没法比。经过了多日的测试后,终于遇到了一个问题,就是游戏玩到某些时候会短暂卡顿,体验很不好,而且这种卡不是通过挡一个进度条就能解决的,因为连进度条都会卡,我不知道什么问题,辗转难眠。有一天晚上盖茨托梦给我说因为我的编码习惯不好,喜欢将有些变量一开始就初始化减少代码行数,结果每次创建某个对象时因为初始化运行了太多东西导致卡,我这才恍然大悟。仔细查看发现有一个比较长的字符串常量在一开始定义时便被初始化了,每次创建对象都会因此卡一下。

    都明白了吧,如果你能确保变量在访问前就会被赋值(或者加一个非法判断(当然你流程合理完全不需要这个判断)虽然不是很推荐这样做),那么不要在定义的时候初始化,这样每当这个模块被创建时初始化会消耗CPU来运算,造成卡顿。

同理可得,有些手机APP在启动的时候可住或黑很长时间也是因为初始化了太多东西造成运算拥堵,CPU在缓慢地疏导交通。

那么必须初始化的常量如何解决呢?问得好!写一个公共的初始化函数,在游戏已经进去或者对象被创建后在主要功能执行前主要变量访问前再调用一次进行初始化,因为这时候往往CPU空闲率会比较高,避开创建的那个锋芒!

    比如你做移动应用,需要3000个常用汉字放到一个字符串或一个数组里,一定不要初始化时直接赋值(那样直接卡死(我说的是移动应用)),而是要分成几百字符为单位长度的若干段来逐渐拼起来,并且是在游戏加载的某个时间做这件事,这样才不至于你的游戏在一开始就卡死。

        (区区5点内容,居然被我写了5000字,真是个搞学术的料啊!!!)

推荐阅读