• 2007-10-31

    一段Ruby代码的解释

    Tag:Ruby
    阅读Rails源码的时候,会发现代码中遍布着一些看上去比较奇怪的代码,大概会是这个样子:
      people.collect(&:name)

    这段代码实际上等价于
      people.collect { |p| p.name }

    但是,从Ruby的语法上来看,这行代码看上去是很难理解的,主要就是&:name。把这行代码当作Ruby脚本直接运行,你会发现,Ruby会向你报错,错误是:
      wrong argument type Symbol (expected Proc) (TypeError)

    那么为什么这样的代码遍布于Rails,却能正常运行呢?

    最初困扰人的可能主要是&:name的写法,这看上去根本不像正常的Ruby语法,但只要将它进行适当的分解,我们就豁然开朗了。在Ruby中,“&”通常用来表示后面跟的是Proc,而:name在Ruby中表示一个Symbol。把二者结合在一起,矛盾变产生了,”&”要的是一个Proc,而我们给的一个Symbol,这就是前面错误的来源。

    知道了错误的来源,接下来的问题是,Rails里究竟变了怎样的魔术,让这段代码通过呢?

    其实,“&”后面如果跟的不是一个Proc,那么它会试图找寻一个Proc,在Ruby中,这意味着它会调用to_proc方法。这是Ruby中的一种标准协议,关于这种转换协议,可以参考《Programming Ruby》中《Duck Typing》一章相关的介绍。

    由于后面的是一个Symbol,结合前面的说法,只要为Symbol类提供一个to_proc方法,至少在语言层面上,就会变得正确起来。事实上,Rails正是这样做的。

    class Symbol
      def to_proc
        Proc.new { |*args| args.shift.__send__(self, *args) }
      end
    end
    (activesupport/lib/active_support/core_ext/symbol.rb)

    如果说to_proc仅仅是让这个魔术在语言层面上通过,那么上面这段代码也解开了其余部分的神秘面纱。不妨再进一步,看看它究竟是如何做到的。

    通过开始的代码等价对比,我们已经知道了这行的代码意义,主要也就是调用了一个对象的方法。将它对应到Proc.new所附带的block上,我们便不难看出这段代码的意义所在了。

    这里的*args是block的参数,在前面的例子中,p就是这个参数,我们要调用p的name方法,所以,p是作为receiver的,而args.shift正是将p提取了出来。通过__send__,我们就可以调用receiver的方法,而__send__需要一个Symbol指定要调用的方法,别忘了,我们正是在一个Symbol类中定义方法,于是self成为了一个自然的选择。至于剩余的参数(对*args调用shift之后),就作为参数传给方法了。实际上,大多数用法中,只有一个参数,所以,剩余的部分会是一个空数组。

    通过这种变换,p.name就等价于args.shift.__send__(self, *args)了。

    关于这段代码的实现,也许Ruby Extensions的实现更加清楚一些。
      def to_proc
        proc {|obj, *args| obj.send(self, *args) }
      end
    这段代码将receiver分拆出来,所以,更加容易理解。当然,严格的说,二者还是稍有差别,这种实现必须要有一个参数。但是,在大多数情况下,这种实现已经足够了。
  • 今天是一个发布的日子,XRuby发布了0.3.1,Ruby Hacking Guide中文版发布了第一部分。

    XRuby 0.3.1

    相比于前一个版本,XRuby 0.3.1最大的进步在于完成标准库的预编译。预编译意味着什么?标准库代码无需在每次运行时编译,这意味着今后使用XRuby的标准库性能会得到一定的提升。

    有一个与编译相关的话题。之前,Jon Tirsen曾经谈到JRuby的一个问题,运行在AppServer中会有占用太多内存。经过分析得知,为了提高程序的并发性,程序运行会启动多个JRuby。每个JRuby解析Ruby脚本都会建立一棵完整的语法树,这就意味着,由于这种解析模式本身的限制,对于同样的内容,内存中需要保存多份相同的语法树,这种做法意味着无谓的耗用了大量的内存。采用编译的做法,则可以很好的避免这个问题。因为在运行时,相同的是字节码,而JVM很好的帮我们解决字节码共享问题,无需耗用大量的内存。

    Ruby Hacking Guide中文版第一部分

    RHG终于完成了第一次发布。已经发布的第一部分介绍的是Ruby的对象模型。我正是从这个部分开始了解Ruby实现的,进而完成了XRuby的Runtime的重写。所以,我一直觉得这部分是了解Ruby实现非常好的一个起点。

    从翻译Ruby Hacking Guide到现在已经超过了一年,从第一次发布消息算起也超过了9个月。相比XRuby,这个项目的进展可以用异常缓慢形容。这是一本日文书,也是一本技术书,而且是一本讲语言实现的书。任何一个点都会增加翻译的难度。几个懂日语的朋友先进行一遍初译,然后,我对再对译稿进行一遍校验,并根据自己的理解修改译稿,这样的过程无疑延长了处理的时间。这是一个业余时间的项目,而我更多的业余时间在XRuby上,没有太多精力投入上面。种种的因素造成了这个项目的一托再托。

    目前,我手头已经有了第二部分全部和第三部分几章的初译稿,不过,按照之前的进度来看,这几章的发布可能要等到许久之后了。如果你有兴趣,可以加入到这个项目中来,这样,有助于加快这个项目的进度。

  • 管窥Ruby——类的变量》写在去年,写成之后便更新了一次,因为最初的描述存在一些偏差。即便如此,jxb8901依然指出了其中的一些不足。最近,dennis-zane再次提出了这个问题。回过头来仔细品味,确实有些地方写得不是很到位,索性把它重新写过。

    管窥Ruby——类的变量

    变量和方法是面向对象难以割舍的两个重要组成部分。在《管窥Ruby——类的方法》中,我们谈到方法,沿着这条路继续,我们再来看看类中的变量。

    开始之前,我们还是要再次回顾RClass的定义:
    struct RClass {
        struct RBasic basic;
        struct st_table *iv_tbl;
        struct st_table *m_tbl;
        VALUE super;
    };
    (ruby.h)

    如果你看过《管窥Ruby——类的方法》,了解了方法存储方式,变量的存储方式便也一目了然了,同样的st_table,意味着同样的处理方式。

    不得不承认的一点是,在讨论类的方法时,我故意忽略了一个事实:方法分为类的方法和实例的方法两种。如果对其它语言实现有些许了解,我们知道,这两种方法差别仅仅是this(C++或Java的说法),到了底层时,这个差别可以视为无物,可以统一存放。关于这点,有兴趣可以参考一下《深入Java虚拟机》,它的第五章讲解了虚拟机内方法的表现形式。在Ruby中,类的方法和实例的方法并不是存放在一起的,这里定义的实际上是实例方法,而类的方法是定义在类的Singleton类中。

    遇到变量时,我们会碰到同样的问题:类的变量和实例变量是无法统一管理的。因为类的变量只有唯一的一份,而实例变量则是每个实例都有一份。所以,RClass存放的并不是真正实例变量。代码是说明问题的最好方式,下面这段代码说明了如何设置实例变量:

    VALUE
    rb_ivar_set(obj, id, val)
        VALUE obj;
        ID id;
        VALUE val;
    {
        if (!OBJ_TAINTED(obj) && rb_safe_level() >= 4)
            rb_raise(rb_eSecurityError, "Insecure: can't modify instance variable");
        if (OBJ_FROZEN(obj)) rb_error_frozen("object");
        switch (TYPE(obj)) {
          case T_OBJECT:
          case T_CLASS:
          case T_MODULE:
              if (!ROBJECT(obj)->iv_tbl) ROBJECT(obj)->iv_tbl = st_init_numtable();
              st_insert(ROBJECT(obj)->iv_tbl, id, val);
              break;
          default:
              generic_ivar_set(obj, id, val);
              break;
        }
        return val;
    },
    (variable.c)

    抛开前面那些复杂的东西,直接来看switch语句后面的内容。这里存在两种情况,对于存在T_OBJECT、T_CLASS和T_MODULE标记的,变量会写入iv_tbl,而其余的情况则转交 generic_ivar_set处理。iv_tbl是“实例变量表(instance value table)”的缩写,不过,如果当前对象是个类对象,这个“实例”变量实际上就是类变量,所以,这个名字多少有些名不符实。

    除此之外,还有一个generic_ivar_set。下面是generic_ivar_set的实现:

    static void
    generic_ivar_set(obj, id, val)
        VALUE obj;
        ID id;
        VALUE val;
    {
        st_table *tbl;

        if (rb_special_const_p(obj)) {
            special_generic_ivar = 1;
        }
        if (!generic_iv_tbl) {
            generic_iv_tbl = st_init_numtable();
        }

        if (!st_lookup(generic_iv_tbl, obj, (st_data_t *)&tbl)) {
            FL_SET(obj, FL_EXIVAR);
            tbl = st_init_numtable();
            st_add_direct(generic_iv_tbl, obj, (st_data_t)tbl);
            st_add_direct(tbl, id, val);
            return;
        }
        st_insert(tbl, id, val);
    }
    (variable.c)

    同样忽略一些非主干的部分,我们看到,这段代码先在一个generic_iv_tbl中进行查找,用作查找键值的是对象实例(obj),而目标同样是一个st_table。得到这个表之后,利用变量名做键值将值插入到表中。我们便不难分析在这种做法中实例变量的存储方式。存在一个全局的实例变量表,走到这里的实例都在其中拥有一席之地,而实例变量存储在实例对应的表中。Ruby通过这样一个二级结构,解决了这些实例变量存储的问题。

    了解过基本的做法之后,随之而来的一个问题就是,这些代码都会在什么情况下起作用。

    代码已经说得很明白了,只有在有那几个标记的情况下,才会直接调用iv_tbl。T_CLASS和T_MODULE都很好理解,那什么情况下会有T_OBJECT呢?在C Ruby中,在Ruby层次上定义的类,生成的实例都是会标有T_OBJECT(参见《管窥Ruby——Allocator》)。所以,所有由Ruby层次上生成的对象都会走到这里来。

    那generic_ivar_set呢?除了那几个标志外的其他部分都会走到这里。除此之外的标志表示什么呢?读一下代码我们便不难发现,几乎就是builtin的几个类,比如数组、字符串、正则表达式等等。那为什么这些builtin类没有一个iv_tbl。对于这些builtin而言,它们真正的实例变量都是以C的形式给出,所以,额外存在一个iv_tbl实际上是一种空间上的浪费。

    虽然不是常态,但我们依然可以为这些builtin类添加自己的实例成员。为了保持Ruby的动态特性,这才有了generic_ivar_set的存在。

  • 方法调用是一种在Ruby运行过程中常见的不能再常见的行为了,据说即便是一个空的Rails应用,也有十万次以上的方法调用。所以,对于XRuby来说,方法调用的性能优劣会对Ruby程序运行产生很重要的影响。方法调用分为方法查找和执行两个部分,其中,方法查找的主要优化手段就是方法缓存,这个问题我已经在《管窥Ruby——方法缓存》解释过了。这里来看看XRuby中对方法执行部分的优化。

    在XRuby中,Ruby方法主要接口如下:
    public abstract class RubyMethod {
        protected abstract RubyValue run(RubyValue receiver, RubyArray args, RubyBlock block);

        public RubyValue invoke(RubyValue receiver, RubyArray args, RubyBlock block) {
            ...
            RubyValue v = run(receiver, args, block);
            ...
            return v;
        }
    }

    这里,run接口是真正的方法内容,而invoke主要是一个外部调用的接口,其中加入了一些校验和参数处理等功能,通常会这样调用。

    在实际中,我们发现大量的方法调用只有0个或1个参数。以1个参数为例,为了适应上面的接口,我们必须把这个参数置入一个RubyArray,再进行调用。
        RubyValue result = c.invoke(receiver, new RubyArray(v), block);

    而run的实现中,再把这个参数取出来。
        protected RubyValue run(RubyValue receiver, RubyArray args, RubyBlock block) {
            RubyValue v = args.get(0);
            ...
        }

    在这个过程中,RubyArray只是起到了一个接口的作用,无论是方法的调用者还是方法本身都不会真正用到它,在这个过程中,创建RubyArray本身就是一个冗余,所以,它就是我们优化的入口点。

    如果没有RubyArray,我们会怎么做呢?我们希望把这个参数直接传给方法,比如:
    public abstract class RubyMethod {
        ...

        protected RubyValue run(RubyValue receiver, RubyObject arg, RubyBlock block) {
            return this.run(receiver, new RubyArray(arg), block);
        }

        public RubyValue invoke(RubyValue receiver, RubyObject arg, RubyBlock block) {
            ...
            RubyValue v = run(receiver, arg, block);
            ...
            return v;
        }
    }

    这样,至少调用者据可以不必显式创建数组了,
        RubyValue result = c.invoke(receiver, v, block);
    不过,在run的缺省实现中,我们依然创建了数组,而且它可以很好的和原有代码进行协作。不过,尽管调用者代码得到了简化,但显然无法满足我们优化性能的需求。所以,我们希望方法实现者也可以利用到这一点。为了简化单参数方法的实现,XRuby提供了一个基类:RubyOneArgMethod。

    public abstract class RubyOneArgMethod extends RubyMethod {     ...
        protected abstract RubyValue run(RubyValue receiver, RubyValue arg, RubyBlock block);
             protected RubyValue run(RubyValue receiver, RubyArray args, RubyBlock block) {
            return this.run(receiver, args.get(0), block);
        }
        ...
    }

    从这段代码中,我们将单参数的方法作为需要子类自行实现的方法,这样,如果调用者直接调用单参数的invoke,它就会调用到这个方法上,这样,便省略了中间数组的创建过程,另外多参数run的实现也告诉了我们方法如何转化为单参数调用。这段代码稍微有些让人困扰的地方是,这里将基类一个具体方法改成了abstract方法,通常我们都是将abstract方法具体化了。这段代码虽然有些奇怪,但这样做确实是起作用的。

    基于同样的思路,我们可以对无参数方法进行优化。在Ruby中,还有一个同方法调用一样常用的是Block调用,幸运的是,由于结构上的相似性,我们可以采用同样的方法进行优化。
  • 或许你已经知道了,XRuby在用Ruby编写builtin

    XRuby很早就开始用Ruby编写builtin,甚至在我加入XRuby的时候就是这个样子了。在这个问题上,比XRuby走得更远的是rubinius,这个项目中用Ruby所写的builtin的比例比XRuby要大得多,事实上,XRuby从中借鉴了不少东西。

    其实,我对用Ruby去写builtin一直有一些质疑,我所能接受的这么做的主要原因是因为这么写来得简单。问题在那呢?我一直以为用Ruby编写builtin性能会很差,因为解释Ruby脚本是需要成本的。所以,我和一些人谈到这个问题时,总会说用Ruby写builtin只是过渡方案,之后可能会回到Java上。直到最近,我才发现,自己的思路有问题。因为XRuby的builtin是预先编译好的,也就是说,解析builtin的成本都在XRuby的构建过程中,所以,在XRuby执行的过程中,用Ruby写成的builtin和用Java写成的builtin本质上没有任何区别,性能上不会有任何损失。这种做法的好处源自预处理,对于编译器产生的代码而言,编译的过程就是运行过程的预处理。其实,解释器也可以做到这一点,比如Python,它会将前面解析的产物(.pyc文件)保留下来,减少运行时处理的内容。但是,目前的C Ruby实现没有这么做,Ruby的脚本必须每次都要经过解析器处理,所以,如果同样用Ruby的builtin,目前的C Ruby实现会比较惨。当然,就成本而言,编译器前端的成本在整个计算中所占的比例往往是很小的,但是,如果没有这个成本,那会更美妙的。

    当然,这并不是说用Ruby编写builtin就完美无缺了。生成代码能否像手工编写的那样简洁,如果不能的话,那就意味着生成的.class文件会有一些冗余在里面。不过,我对这个问题倒是没有太多顾虑,因为编译器是XRuby主要的成果物,所以,这个问题真的是问题了,我们完全可以通过自己的努力,把它改进得更好。

    还有一些问题就不那么容易解决了,这几个问题在XRuby的blog上已经讨论过了,这里只是简单介绍一下。

    首先,还是性能。用C Ruby写的builtin在很多部分存在优化,这是Ruby写的代码无法做到的。比如Integer#times。如果用Ruby写大概会是这样:
    class Integer
        def times
            i = 0
            while i < self
                yield i
                i += 1
            end
            self
        end
    end

    看上去这个实现很简单,但无论是“<”,还是“+”,都需要调用方法实现,而我之前介绍过Ruby中的方法查找,显然,对于普通的Fixnum而言,它远不如直接在Java代码中些for循环来得快。

    再有,就是Ruby的类是开放的,如果一个方法中用到另一个方法,那么被调用的方法如果被修改的话,就会对调用端的方法产生影响。还是前面这个Integer#times,显然,如果修改了Fixnum的“<”或是“+”的实现,就会对times的实现产生影响,这会造成与C Ruby实现上的不兼容,因为其中对Fixnum进行了优化,不会受到Ruby层次上“<”、“+”实现的影响。当然,在XRuby的blog上,有人提出的了一些解决方案,但在我看来,这样的解决方案只是可以实现,却完全没有美感可言。

    用Ruby编写builtin不像初看起来那么差劲,却是需要小心翼翼。