博客

  • 评《最容易被人忽略的基本代码优化技术》

    这篇文字主要来自我在TopLanguage讨论组里面的发言,仅作总结留记。

    首先要介绍一下原文作者Walter Bright,从他主页可见他是一个很牛的程序员,主要工作领域是写编译器。这个背景很重要,这是原文第二条的主要基础。但是也要注意到,一百个程序员里面也许都没有一个知道如何编写编译器,知道如何读写汇编的程序员比例也许会高一些,但不会很高,估计也就是百分之几的样子,比如我只会读一些简单的汇编代码,但是不会写。

    这篇文章认为Profiling和看汇编语言如何被运行的是两个基本技术,我在讨论中也没有提到写汇编,因为会读的人要比会写的多出不少。但是看汇编真的不是一种基本的优化代码手段。

    首先这个代码就很值得讨论,Javascript算不算代码?Ruby on rails或者Django这些框架算不算?或者Mono、Java、DotNet算不算代码?这些领域的程序员,很少有人会从汇编语言层面来做代码优化。如果你是一个类似项目的Team leader,组员说他最近在读某Rails项目的汇编代码,想找出慢的原因并加以改进,建议你立刻把他开掉,因为要么他是在磨洋工,要么是脑袋坏掉了。

    Profiling是优化代码的必要手段,而不是什么可以被忽略的技术。实在无法想象没有做Profiling,如何找出系统瓶颈,如何做代码优化。换句话说,没有Profiling其实也就没有基准数据,怎么保证你修改的代码一定是优化的?!从这一点上说,忽略Profiling来进行优化,很难想象。

    关于Windows下的Profiling,我以前写过一篇博客Profiling C++ performance in Windows http://sunxiunan.com/?p=1216 (名字虽然是英文的,但的确是我写的),里面介绍了一些我看到的或者用过的Profiling技术,颇为简陋,但也帮我做了不少代码优化相关的工作。

    正常一个通用的代码优化过程应该是这样的:

    1)用户提出性能有问题。如果没有人提出,那就尽量不要作修改,因为优化往往会带来更为混乱的代码,或者一些很难理解维护的代码关系。

    2)询问用户他们期望达到的运行速度是什么。因为优化往往是在这里做做在那里改改,如果达到了期望值就要立刻停下来,否则过犹不及。

    3)对当前系统做Profiling,收集运行数据。要注意Profiling会多少导致系统运行速度变慢,如何获取正确的运行数据,需要做一些编程工作。并不是拿来一个工具就一定会找到问题。另外还要确认Profiling数据本身是正确的,否则优化多半会是徒劳的。

    4)取得数据以后对系统做逐步改进。这一步要注意的是每一步改进都要尽可能的独立、可回退,每做一步改进都要做一次新的Profiling,保证修改以后的代码真正有了速度上的提升。如果对性能提升效果不明显,就不要引入这个修改,哪怕你在上面投入了很多精力,要敢于舍弃。另外做代码优化要着眼全局,不能局部优化了,但是整体运行速度下降。另外要注意对于每一次修改,应该有相应的测试手段保证修改不会影响系统运行。基本上这些优化代码都会引入一些新的bug。关于性能优化,有一篇很不错的文章可以参考http://csbabel.wordpress.com/2010/04/29/interviewbible-1-speed-optimization/

    5)代码优化达到期望值以后,停止优化过程,记录相关数据以及优化方法,留作以后查询。

    我们这里只提代码优化,其实优化还有很多其它手段,比如添加更好的硬件,换一个快的数据库或者做做数据库调优,改改系统参数,换一个更快的webserver,优化本来就是一个系统工程,光看某一个点很难做好优化,应该有全局观点。

    至于看汇编代码找问题,主要是针对C程序员或者C++程序员。但是要知道现在的编译器技术发展与8086时代是完全不一样了,编译配置是完全优化的代码,你很难从运行中看出问题,产生的汇编代码也很难读懂。这也是我很不推荐大家用看汇编来做优化的原因,有时间可以找到更多更严重的系统瓶颈。如果要从汇编层看问题,首先要看懂汇编代码,然后还要会改汇编代码,投入产出比例差太大。而且找到问题就看汇编,容易陷入细节,要时刻牢记优化应从大处着眼,抓大放小才是硬道理,我算是一个比较普通的VisualC++程序员,也做过一些简单的性能优化,基本上用不到看汇编就知道问题所在,profiling是最有用的也是唯一的工具。当然对于作者这样写编译器的程序员而言,看汇编写汇编是他们的日常工作,那就另当别论,但不能推而广之。

    另外需要强调的是,虽然不建议大家在优化时看汇编,但对于程序员来说,学习时深入到汇编这一层是非常有帮助的。比如一些常见的问题如calling conversions(http://sunxiunan.com/?p=1229),i++和++i有什么区别,const一些研究http://sunxiunan.com/?p=870 都是看到汇编代码以后就比较容易理解。

    总而言之,任何一个程序员要知道如何做代码优化,应该了解代码优化的基本步骤,自己相关领域的一些常见性能问题的模式,自己工作领域常见的Profiling方法和优化工具。有的放矢才能更好的工作。

  • KISS理论乱掰

    KISS理论不是一个感情方面的娱乐节目,而是编程领域里面很重要的设计思想。

    电脑编程与其它方面的科学相比,总差了一些严谨性或者理论不是那么完善,很多理论都是基于猜想说话,而没有数据支持。不过KISS这个理论我是认同的,从某种程度上讲,KISS很像是奥卡姆剃刀原理,越简单越正确。

    KISS是什么意思?这个就众说纷纭,也可见电脑编程之不科学。有人说是Keep it simple, Stupid!翻译过来就是,“简单点,傻瓜!”很有一些Nerd傲视群雄的气派,但是我喜欢翻译成“要简单,要尽量傻!”(也就是stupid翻译为形容词),理由后面会解释。在维基百科上http://en.wikipedia.org/wiki/KISS_principle介绍了KISS理论的一些相关概念,还有一些人名、链接什么的让你可以跑题。

    为何KISS理论很有趣很重要?我个人感觉,KISS理论让人时刻牢记,人是傻的、或者说大部分都是傻的,而编程代码是给人看的,所以复杂代码复杂算法复杂函数都会导致后续维护的困难,从软件工程角度上说,编码阶段与后期维护阶段时间基本上是1:N的关系,越复杂的软件N值越大。

    以前一个编程书籍阅读小组,想用UML来描述一个网页游戏的架构,结果对于一个类应该属于这个还是那个争吵不休,看了让人觉得很好笑。一个类属于A还是属于B是不是很重要的事情?depends on,但是对于五五开的问题来说,真的没必要争论几天时间,还不如选一个简单的方案先实现出来。

    还有常见的简单问题复杂化的例子是代码重用以及接口扩展。有些人觉得自己编程水平高,言必称重用。比如C语言没有类如何重用代码?都是似是而非的一些问题,连重用是什么这样基本概念都不知道,就敢张嘴乱讲,真真是2B了耶哥哥。接口扩展复杂化的例子在windows api里比比皆是,很多函数就在文档里指明,这个参数应该用NULL,这不扯么,都没有用的参数,当初怎么加进去的?!

    程序员为什么喜欢把代码复杂化?当然是有原因的,首先一个就是让人看不懂,而未知的东西都让人有一种神秘感恐惧感,这样程序员就感觉自己对代码有掌控力,你们都不行就我知道怎么改,你敢让我走路么?!另外一个原因是,复杂化的概念让人觉得有价值,比如你看最近《程序员》上关于云计算的文章,国内这些砖家穴者都纷纷出来推崇云计算,但是文章内容毫无价值臭的跟狗屎一般,都是一些大词(big word)的堆砌繁衍。我在twitter上与程序员主编问过,他无奈回答是”商业需要“。另外一说数据库,一定是Oracle最好,开源的不可靠,为何?因为Oracle那多复杂,连调优都能整成一个赚钱的职业。

    一个程序员要时刻牢记“简单化、傻瓜化”编程,代码要一目了然;长的函数要变短,要不怎么称得上简单;复杂的条件判断要分解,要不怎么称得上傻瓜化,太复杂还得用笔算是不是;至于变量名字,就越直白越好,比如bool IsThisGirlBeautiful(),这种函数一看就懂,连文档都不用写。

  • 一些老照片

    真的是非常老的照片,基本都是八、九年前的。

  • C语言表达式计算顺序的一个小问题

    浏览Q.yuhen的博客这篇文章 http://www.rainsts.com/article.asp?id=959 发现一个小问题,估计有类似想法的同学也有,所以记录一下。

    问题在于这句话:

    “很显然,依据 cdecl 规则,"printf(…, test(2), test(1))" 中的 printf 函数参数依次从右向左 "入栈"(暂且用这个说法)。因此 test(1) 被先调用,然后才是 test(2),上面的汇编代码也说明了这点。”

    尽管事实是这样的,但这是一个有问题的说法。

    cdecl的入栈顺序是没错的,这个入栈顺序是针对每个逗号分隔的表达式结果而言。也就是说对于每个结果一定是这样的顺序。但是表达式计算顺序(或者说每个逗号分割的函数调用)其实是没有规定的。这在K&R影印版第二版52页最末一段说的非常清楚,下面的f和g不一定谁先调用:

    C, like most languages, does not specify the order in which the operands of an operator are evaluated. (exceptions are && || ?: and ",") for example x = f() + g();

    另外在63页开始:

    The commas that separate function arguments, variables in declarations, etc., are NOT comma operators, and do NOT guarantee left to right evaluation.

    这也就是说逗号分隔的函数参数与逗号操作符是不一样的,不保证从左至右的计算顺序(当然也没有保证从右至左)。

    注意这个evaluation,其实就是对test(2), test(1)的调用。如果这些调用有边界效应,在不同编译器、操作系统上可能会得到不同的结果。另外类似的问题有对在参数列表中,同一个变量调用++两次以上。

    如果要保证求值顺序(注意不是求值结果的入栈顺序),只能用临时变量保存调用结果,或使用逗号操作,&& || :?这样可以保证求值顺序的操作才行。

    其实我在前面这篇博客已经提到了这个问题,只是看到这个提法再重申一次。“C语言中的表达式求值问题http://sunxiunan.com/?p=1684

    参考资料:

    http://www.andromeda.com/people/ddyer/topten.html 参考第七条

    C陷阱与缺陷 3.7求值顺序

  • 能打印出自己的代码

    任务:写一段C语言程序,打印出自己整个代码,不能差一分一毫。

    这个任务在计算机编程中有个术语叫做:Quine,维基百科上有专门的条目介绍。

    http://en.wikipedia.org/wiki/Quine_%28computing%29

    quine代码好像没有太大作用,但是我们可以将其想象成一种可以自我繁殖的生物,每次运行就产生一个同样的实体,然后一个个这样繁殖下去,哇塞,这就是恐怖片了!

    搜索了一下,基于C语言(使用VC2010编译必须设置language为C才可以)主要有以下几种比较简洁的实现:

    main(a){a="main(a){a=%c%s%c;printf(a,34,a,34);}";printf(a,34,a,34);}

    这算是第一种模式,关键在于%c%s%c这个打印格式,然后输入参数中多半有34或者0x22(也就是双引号)或者引用到字符串数组中双引号的位置。

    另外一种是通过宏定义实现的:

    #define T(a) main(){printf(a,#a);}
    T("#define T(a) main(){printf(a,#a);}\nT(%s)")

    宏定义的实现消除了对双引号的使用,格式更为灵活一些,不需要是abab这种模式了。

    关于quine一个非常详细的论文,介绍了深层理论、如何写quine代码等等,可以看看。

    http://www.madore.org/~david/computers/quine.html

    另外可以参考:

    http://www.c4swimmers.esmartguy.com/selfcodeprint.htm

    http://www.c2.com/cgi/wiki?QuineProgram