• beanworms问了一个有趣的问题,下面这段Ruby代码的输出是什么?为什么?

    class A
        def m=(value)
            @m = value
            3
        end
    end

    a = A.new
    puts a.m = 5

    按照常规的理解,“a.m = 5”会转换为一个函数调用,最终调用到“m=”。我们都知道在Ruby中,方法中如果没有显式的“return”,那么最后一个表达式的值将作为这个方法的值返回。根据这个推理过程,那么这段代码的输出应该是“3”,因为“m=”这个方法的最后一个表达式是3。

    很遗憾,不是。那真正的结果是什么呢?运行一下就知道了,是5。

    怎么会这样?没什么大不了的,理想和现实之间总是存在些许的差别。有怨天尤人的工夫,我们不如把问题的原因找出来。

    我们说过,“a.m = 5”会被转换为函数调用,而这个转换工作是由Ruby替我们来做的。通常Ruby程序的执行过程是,将程序解析成一个内部的语法树,然后执行这棵语法树。所谓执行这棵语法树,就是根据节点的不同类型,采用不同的方法进行处理,差异便在这里。通常的方法调用会被解析成一个类型为NODE_CALL的节点,其处理过程简化后如下:

    case NODE_CALL:
    {
        ...
        result = rb_call(CLASS_OF(recv),recv,node->nd_mid,argc,argv,0,self);
    }
    break;
    (eval.c)

    我们可以看到这里的最终的返回结果就是方法调用的返回值。但是对于属性赋值,则会解析为一个类型NODE_ATTRASGN的节点,它的处理过程和NODE_CALL类型略有差别,其简化后如下:

    case NODE_ATTRASGN:
    {
        ...
        rb_call(CLASS_OF(recv),recv,node->nd_mid,argc,argv,scope,self);
        result = argv[argc-1];
    }
    break;
    (eval.c)

    从这段代码中,我们发现在这个过程中,确实是调用了函数,但是函数的返回值被丢弃了,而这个表达式的结果直接取自参数。想来也对,通常我们赋值表达式的值都是等号右边的部分。

    看过了代码,我们就很容易解释前面那段的代码了。这里的关键就是“a.m = 5”会解析为一个属性赋值的表达式,而非通常的方法调用,这个表达式的值就是右边的参数,而不是方法的返回值。“m=”作为一个方法,它的返回值确实是3,下面这句代码就可以验证这一点:
    a.send(:m=,  5) => 3

  • 2007-04-10

    合并Runtime

    终于将New Runtime的代码合并到XRuby的trunk中了。

    从开始写New Runtime到现在快有半年了,时至今日,才把它的主体结构合并到trunk中。主要原因是New Runtime和原有的Runtime差异太大。虽然二者起到的作用是类似,但结构和接口上的巨大差异使得合并成了一件很困难的事情。

    如果我没有记错的话,这是第三次进行合并的尝试。yawl在我写好了New Runtime之后,就进行了第一次尝试,试图将编译器移植到New Runtime上,结果很长时间代码都不能运行。虽然这次尝试没能成功,但是从New Runtime得到的继承结构应用到了trunk中。第二次尝试是在0.1.0发布之前,我亲自操刀合并。思路同上次类似,我直接把New Runtime的主要结构合并进来,之后,修改编译器。当时的代码已经可以运行很大一部分的代码,但依然是问题多多,最终没能赶在0.1.0发布之前完成。不过,这次的尝试倒是让我对代码生成部分有了一个比较清晰的认识。

    在0.1.3发布之后,我开始了第三次尝试。谈到New Runtime,yawl给了我个建议,一点一点来,不要试图一下子就把所有内容加进来。从善如流,我没有沿袭前两次那种大刀阔斧的修改方式,而是采用了小规模重构的方法。首先是对执行性能影响较大的Method Cache。说起来很简单,就是加一个简单的缓存,但因为已经存在了大量的测试用例,所以,让所有测试都通过是一个考验。这次修改基本上只改动Runtime的内部实现,几乎不动编译器端,所以,相对来说,影响还比较小。当所有测试用例通过的时候,我长出了一口气,至少成功的迈出了第一步。这次修改同时还加入了ID替换String的实现,所有测试用例通过之后,为了能够更充分的利用ID带来的优势,我又修改了编译器,让它直接生成利用ID的代码,相对来说,这步要简单得多。

    接下来的是更为复杂的类结构,同样,我依然采用的是小规模重构的方式。在开始之前,我划分了一下需要做的几件事,大体说来,分为三个部分singleton类的支持(用于支持为对象定义方法和meta类)、include类的支持(用于支持包含模块),整体结构的支持。

    按照顺序,我开始加入singleton类的支持时,我发现如果想让singleton类很好的运作,必须有对整体结构的支持,这两个工作关联性很大。为了不让自己陷入长时间的一无所获,我放弃了正在进行的singleton类的支持,转向include类的支持。果然,include类的支持相对来说,影响要小一些,编译器部分几乎没有任何修改,所以,很快通过了测试,这样,New Runtime已经有一部分进入到trunk之中。

    再回到singleton类支持时,有一少部分代码因为include类的原因,已经进入到trunk之中。其实,单纯支持singleton其实很容易,而且也几乎不要修改编译器,但正如前面提到的,如果让singleton类能够运行起来,必须要有整体的结构与之配合,所以,在有测试未通过的情况下,我就把整个的类结构全部合并进来了。之所以会很复杂,因为除了类结构本身,还要有编译器部分与之配合。这样,在应对Runtime修改的同时,还要考虑编译器生成代码的正确性。好在二者的表现通常是截然不同,在执行时出现的各种各样错误一定属于Runtime,而编译器的错误是代码根本不能正确执行,加载时就会出校验错,所以,一般很容易知道是哪里出了问题。无论如何,长时间的测试不能通过是一件很痛苦的事情,稍感欣慰的是,总有一点点的进步。

    惊喜总是不期而至,当我还在等待下一个错误的时候,突然所有测试用例通过了,那是一种很奇妙的感觉。虽然现在的Runtime还有不少要调整的地方,但是主要的结构已经进来了。回顾前两次的合并,之所以没能成功,主要是因为目标太大,一次要完成所有任务,很难很好的把握其复杂度。失败中也有收获,如果没有前两次的尝试打下的基础,这次的合并可能会更加复杂。

    合并New Runtime可是我在0.2.0要实现的一个重要目标,实现之后,便可以继续前进了。现在New Runtime不复存在,只有XRuby Runtime。
  • 最近一个月时间,周围的所有人在做的是一件事,优化。让一个物理模拟的程序尽可能快的运行在多核平台上。一个月前,知道要做什么,但心里完全没有底,一个月后,坐在计算机前看着程序运行的录像时,心里还是挺有成就感的。当然,优化是一条无止境的路,即便程序做到目前的状态依然有很大的提升空间,这里只是简单总结一下之前用到的一些方法。

    硬件
    CPU
    说过这是一个多核平台的优化,所以,多核CPU是肯定的了。身边这台机器是两个4核CPU,也就相当于有8个核。但是,多核CPU显然不会像单核CPU提升频率那样可以坐享其成。不做修改的结果只能是看着一个核累死,其它的核闲得无聊。所以,有了多核CPU,还要有软件配合。

    显卡
    另外一个提升的是显卡,因为程序本身有显示的部分,而且是一个3D的图像。后来证明,计算的部分很快,显示的部分成了瓶颈,原来的显卡程序只能运行在简单的模式下,为了能够在更复杂的模式下让程序流畅的运行,换了一块五大三粗的显卡。回忆了一下,为了这个程序而在换的显卡就有四块之多。

    程序
    程序的部分才是我们真正着力去做的事情。

    分离计算和显示

    这是在通常的桌面程序最容易想到的方案,即便不是为了优化。分离二者之后,就不会出现因为计算而造成的界面失去响应的情况。这里用到的技术,主要是双缓冲,计算线程将算的结果放置到写缓冲中,然后翻转。用这种方式将二者分开,避免原来因为同步造成的其中一个的缓慢影响到另外一个。

    计算部分
    程序的核心是计算部分,它真正决定程序的效果。

    多线程
    如果软件不修改,只有多核的CPU本质上不会带来什么提升。现阶段,利用多核最好的方式就是多线程。原本打算自己来写线程管理的部分,考虑到复杂度,最终用的是OpenMP,只要几条编译指令,程序便拥有了多线程的本事。

    数据结构
    很多数据结构在单核的情况下,表现良好,而在多核的情况下就稍显不足了,比如链表。如果需要对链表所有元素进行处理,在单核情况下,遍历就好了,而在多核的情况下,简单的遍历是不能充分发挥多核的优势,而且往往需要用锁保证并发访问的正确性。如果用数组的话,只要能够进行正确的划分,比如,每个线程处理一段,那多核的优势便会发挥出来。所以,有些时候,为了发挥多核的威力,需要适当的调整一下数据结构。

    显示部分
    本质上来说,显示部分的调整并不能让程序算得更快,但它却可以给人们觉得“程序算得更快”。

    局部视图
    当程序只显示一个局部的时候,把所有视图都画出来其实没有什么意义,而且还要浪费大量的计算资源,所以,只绘制可以显示的一小部分即可。

    远端视图
    程序中的物体是一个3D模型,但是,我们都知道近大远小的道理,当它看上去离我们很远的时候,根本看不出来到底是个什么东西。所以,在这种情况下,我们可以用简单的模型替代复杂模型,提高处理性能。

    软件
    编译器
    为了压榨程序的性能,一个好的编译器自然是不可或缺的。所以,我们选择了号称能生成Intel平台最好性能代码的ICC。除了编译器本身的优异,它对OpenMP的支持也让我们可以放心这个方案,另外,它还提供对SSE系列的支持,可以省去思考汇编的烦恼。

    Profiler
    当大的方面已经就绪,剩下的细节就是Profiler展现本事的地方了。VTune是个不错的选择,可以让把程序运行的状况清晰的展现出来。通过对VTune捕获事件的定制,我们可以要求它给出我们希望了解的内容,比如缓存命中的状态,不看不知道,原来因为Cache Missing造成的损失还是不少的。于是,它为我们进一步调整提供了一个方向。

  • 我喜欢Ruby,因为它在我心中是一种优美的程序设计语言;我喜欢读纸版书,因为捧在手里很有感觉;我喜欢读中文版,毕竟中文是我的母语。所以,当《Programming Ruby中文版》面市的时候,我迫不及待的在网上下了订单。

    这本书给我的第一印象是“厚”,有如字典一般的外观对于捧在手中的阅读不会是一种非常好的阅读体验,不过,几百页的图书想薄也难啊!不过,整体来说,这本书的制作质量还是相当不错的,让人感觉很舒服,事实上,这本书读起来感觉确实不错,也不枉为它等待如此之长的时间了。

    因为已经对Ruby有了一定了解,所以,我读这本书更多的是对知识上的查缺补漏,这样的读书,速度自然也快了许多。所以,很快就把正文中我感兴趣的部分读了一遍。至于后面库的部分,用到的时候再说吧!

    这本书几乎算得上Ruby的“圣经”了,无论是厚度还是内容。它几乎涵盖Ruby语言的各个方面。作者在写书方面是老手,所以,很会照顾不同层面的需要:想了解Ruby,第一部分提供的就是一个简单的教程;想深入语言的细节,第三部分把Ruby语言的方方面面展示了出来……

    让我最有感觉的是第24章《类和对象》,之所以这么说,是因为其中把Ruby的对象模型明明白白的摆在了那里,刚好是近来一直在用功的方面,当然看得酣畅淋漓。相对于语法是形,背后的思想就是神了,而把Ruby的神很好的展现出来的是第23章《Duck Typing》,如果对象能够像鸭子一样走,像鸭子一样叫,那么就可以把它当作鸭子。更准确的说,这章是从静态语言迈入动态语言所需的真正转换。

    如果非要要为这本书找些毛病,我能想到的就是它对Metaprogramming涉及很少,毕竟DSL(Domain Specific Language)已经越来越受到人们的关注,而Ruby在Metaprogramming上的能力恰好可以在DSL上大有作为,ROR已经为我们很好的展示了这一点。

    对于有些人来说,Ruby就是ROR,但实际上,Ruby要比ROR更为广阔。所以,单纯从学习ROR的角度来读这本书,无疑是有些过了。据说有本《Ruby For Rails》很适合为了ROR而学Ruby的人,没读过,仅把把它作为信息列在这。像我们这样通过书来学习新知的人,很重要的就是找对书,因为上错了船而大骂船的人,真正该骂的是自己。

    对于中文版的图书,翻译是一个无可避免的话题。这本书的翻译质量很高,很少出现因为拗口的字句打断阅读的现象。不过,还是有些细微之处存在瑕疵。比如,在346页上,谈及方法定义时,有这样一句话:
        方法定义可能不包含类或模块定义。
    其原文是
        Method definitions may not contain class or module definitions.
    我对这句话的理解是
        方法定义不可以包含类或模块定义。
    如果按照原译文来理解,显然既然方法定义“可能不”包含类或模块的定义,那当然也“可能”包含了。实际上,只要稍微试一下,Ruby就会告诉你这是不可以的。显然,译者对某些技术细节的把握还稍有欠缺。

    喜欢吹毛求疵的我,在看书的同时,给书挑了一些细小的毛病。可喜的是,无论是译者还是编辑都能够对此积极的响应,这样的鼓励使得我有了“变本加厉”的动力。

    《Programming Ruby》是一本不错的好书,但是我们不可能指望任何一本书告诉我们世上的所有事情,《Programming Ruby》也不能,它只是一条通往Ruby世界的桥梁,接下来更多的还是要依靠自己的努力。如果读了这本书发现Ruby还有些吸引自己的地方,那就进入Ruby的世界享受编程的乐趣吧!
  • 2007-03-31

    扰人的选择

    人这一辈子总会有许多选择,小到穿什么衣服、吃什么饭,大到未来的人生路。选择是一个令人头疼的问题,但凡有选择的机会,就意味着有一大堆要对比的东西,而且即便是同样的东西,不同时刻摆在面前,因为中间经历了许多的变化,其选择也会有所差异,比如不想吃昨天刚刚吃过的东西。

    有时候,选择之前会有个预期,这个预期也许是基于之前种种考虑做出的最为顺应心灵的选择,但这个预期的根基往往只是“考虑”出来的,而非真实摆在面前的种种。一旦迈出一步,真实摆在了面前,突然会发现,这个世界总有很多超出预计的东西,前面用心良苦做出的选择,瞬间便面临着土崩瓦解。虽然漫无目的的行事绝非值得推荐的实践,但过度的思考也绝对是在浪费时间。

    也许是饱汉子不知饿汉子饥,我突然觉得没有选择或是只有一条路是最好的选择。选择的滋味并不好受,尤其是面对着多个不忍放弃的目标,而事实让人只能选择其中一条路时。有时,真的希望自己有分身之术,这样,不必劳心费力的去做选择,每条路上一个分身就好。遗憾的是,这项技能不属于我等正常人类。

    一个与选择有关的词最近经常有人在我耳边提起:平衡。有些选择,单从某个方面上看起来,是简直无懈可击,美好得让人容易迷失,让人容易义无反顾,但且慢,它真的是最合适的选择吗?有所得必然有所失,再美好的选择也一样,这个方面的得是不是意味着另一方面的失呢?一俊确实容易遮白丑。最合适的选择,应该是那些在各个方面达到了一个相对平衡的选择。

    人在处于选择的十字路口时,总会有许多的声音从四面八方涌来。这些声音往往只能让大脑更加混乱,尤其是那些上升到仁义道德的声音,时常会让人觉得某个选择会愧对包括自己在内的所有人。这些声音有自己正确的部分,但事实未必那么可怕,真正让自己做出选择的应该是自己的心,至于那些声音,很好的参考。

    选择了,就要走下去。这个世界没有一种叫做“后悔药”的东西,这也是某些选择更为痛苦的原因。后悔是没有用的,所以,既然选择了,唯有坚持的走下去,即便没有得到预期的东西,也会有一些额外的收获。

    如果读这篇不着边际的blog有云里雾里的感觉,没有关系,只是说明没有类似的经历,不代表别的。仅以这篇blog献给和我一样与选择作战的人们。