• 2007-02-02

    体验YARV

    YARV,Yet Another Ruby VM,又一个Ruby VM。

    其实,我觉得它的名字算不上准确,因为原来Ruby算不上拥有VM。当然,按照作者的本意,这个名字不会长久的存在下去,因为项目如果成功,它就会成为Ruby的一部分,不成功的话,没人会记住它。事实上,它正一步步走向成功,因为现在它已经被合并到Ruby的主干代码中。

    我不想苦苦等待YARV随着发行版来到我们身边,于是我选择提前体验YARV。到Ruby的网站上,下载Nightly Snaphot版本,我们就可以拥有最新的Ruby版本,当然,这样做的代价是这个版本可能不如正式的发布版那样稳定。事实上,我自己在编译的时候就遇到了一些小问题,幸好,简单修改一下便万事大吉。

    关于如何编译Ruby,我已经在之前的blog上讨论过,这里就不再赘述。

    下面是我用来体会YARV的一段代码:
    def fibonacci(n)
        if n == 1 or n == 0
            n
        else
            fibonacci(n - 1) + fibonacci(n - 2)
        end
    end

    start_time = Time.new.to_f
    puts start_time
    puts fibonacci(30)
    end_time = Time.now.to_f
    puts start_time
    puts end_time - start_time

    首先,我用Ruby 1.8.5-p12运行这个版本,我最关心的运行时间大概是:
    2.60199999809265

    同样一段代码用加入YARV的Ruby来跑:
    0.401999950408936

    大幅度提高,看来YARV果然不负众望。如果希望继续体会YARV,现在Ruby的代码中还多了一个benchmark目录,里面全是一些耗时的运算。

    既然YARV号称虚拟机,自然要有自己的指令集。JRuby正在开发的代码之中,已经开始提供对YARV指令的支持,Ola Bini在自己的blog上写过几篇与之相关的blog:
    Executing YARV (in JRuby)
    YARV tail call optimization in JRuby

  • XRuby是什么?它是一个编译器。与其它编译器一样,它完成的工作是将一种格式的语言转换成另一种。与大多数编译器不同的是,它是将Ruby的代码(.rb)转换成Java的bytecode(.class)。XRuby是一个开源项目,很荣幸,我是这个项目的成员。

    提起Ruby和Java的组合,现阶段,在人们脑中率先撞线的多半是JRuby。JRuby确实在结合Ruby和Java这条路上走了很长一段时间,尤其是去年SUN吸纳了其几个开发人员,名声一下子壮大了许多。但是,JRuby只是一个用Java开发Ruby解释器,而非编译器,也就是说,它并不是将Ruby代码编译成Java的bytecode。当然,现在JRuby的开发者已经走上了编译这条路,不过,仅仅是刚刚起步。从这个意义上来说,XRuby走在前面。

    故事得从头说起。2005年中期,yawl一个人开始了XRuby的开发。项目的最初,开发的只是一个Ruby的语法解析器,所以,那时候的名字叫做RubyFront。在yawl的blog上,不难发现他在开发过程中的一些心路历程和解决的各种各样的问题。经过艰苦的努力,RubyFront发布过两个版本,0.1.20.2.0。到0.2.0的时候,RubyFront已经可以解析Ruby On Rails的1.1.2版本了。

    一个人的努力总是有限的,yawl在孤独前行了很长一段时间之后,决定把这个项目开源,吸引更多人一起努力。2006年9月8日,yawl将代码移至Google Code。此时的项目已经超出了一个Ruby语法解析器的范畴,于是,项目名也由RubyFront变成了XRuby。至此,XRuby正式诞生,也开始向一个真正的Ruby编译器迈进。

    项目是开源了,但如果还是一个人做,那和不开源是没有分别的。于是,yawl决定为XRuby打第一次广告,吸纳开发人员加入到XRuby中。

    我就是这个时候加入的。当时,刚好完成了RHG第一遍的翻译,对Ruby的构造有了初步的理解。看到这样的消息,当然是兴奋不已:自己熟悉的Java、自己喜欢的Ruby、自己想要了解编译器技术。通过与yawl的沟通,我成为了XRuby这个项目的成员,除yawl之外,第一个加入这个项目的成员。

    最初的时候,我花了大量的时间去理解yawl的代码。不过,同我之前了解C Ruby的实现有不小的差异。后来我才逐渐弄清楚,原来yawl的代码更多的是根据自己的想法在写,对C Ruby参考的不多。这样做当然有一些好处,不过,也确实遇到了一些问题。所以,后来我按照C Ruby的实现重新写了一个Runtime,但是由于与原来的实现差异很大,所以,暂时没有加入到第一个版本的发布中,希望在下一个版本的发布中可以看见它的身影。在这个过程中,加深了我对Ruby实现的理解,于是,就有了一篇篇的《管窥Ruby》。

    在XRuby的开发过程中,发生了一件有趣的事。

    有一个叫rubygrammar的项目,目的是开发一个基于Antlr的Ruby语法前端。这个项目聚集一些人气,前期的结果并不太理想,但是受到了很多人的关注。Antlr的开发者Terence Parr就在Antlr网站上上传了这个项目的前期成果,一个半成品的Ruby前端。

    JRuby的开发者Charles Nutter想替换掉JRuby中基于YACC的前端,因为它并不是一个很好的选择,于是,他找到了这个项目。一群人在邮件列表中讨论的结果是,发现现在已经有了一个可以工作的Java的Ruby前端,就是XRuby的前端(也就是最初的RubyFront)。他们建议把前端从XRuby分离出来。结果就是yawl把XRuby的前端贡献到rubygrammer中,成为了其项目的一部分。

    随着开发的进行,又有几个人加入到XRuby中。截至XRuby发布第一个版本时,除了yawl和我之外,成员还有beanworms,javachina和cpunion。每个人都为XRuby出了自己的一份力。yawl说,没有大家的共同努力,XRuby的进度会更加缓慢,我说,没有yawl,XRuby就不存在。每个人都有自己的精彩,在这里,我无法尽述大家在项目中所起到的作用,但相信大家都和我一样,通过XRuby学到了很多东西。

    我不喜欢用国界来区分技术,因为我相信技术是没有国界的。但是不得不说的一点,目前的几个项目成员都是华人,尽管他们有的身在异国他乡。虽然大家在邮件列表中的交流依然使用的是英语,但私下里的中文交流促进了彼此的了解。

    现在,XRuby已经发布了第一个版本。事实上,它还很弱小,远远无法达到我们心目中完美的境界,但它已经迈出了第一步。我们愿意看到它一点点成长。刚刚起步的XRuby需要更多人的帮助,它还在发展之中,有很多的机会可以让人发挥自己的才智。不要为编译器这么“高深”的东西所吓倒,事实上,参与这个项目之前,我对编译器也是知之甚少。XRuby为我们提供了这样一个学习提高的机会,只要拥有热情,我们可以学会那些所谓的“高深”。

    Ruby的创始人Matz总是把乐趣挂在嘴边,如果你有兴趣,就来和我们一起体验这份开发的乐趣吧!

  • 在《管窥Ruby——类层次结构》中,我们已经见识过Ruby是如何实现包含一个模块的,不过,那里的侧重点不同,我们忽略了很多东西。

    让我们回顾一下那段代码:
    void
    rb_include_module(klass, module)
        VALUE klass, module;
    {
        VALUE p, c;
        int changed = 0;

        rb_frozen_class_p(klass);
        if (!OBJ_TAINTED(klass)) {
            rb_secure(4);
        }
      
        if (NIL_P(module)) return;
        if (klass == module) return;

        if (TYPE(module) != T_MODULE) {
            Check_Type(module, T_MODULE);
        }

        OBJ_INFECT(klass, module);
        c = klass;
        while (module) {
            int superclass_seen = Qfalse;

            if (RCLASS(klass)->m_tbl == RCLASS(module)->m_tbl)
                rb_raise(rb_eArgError, "cyclic include detected");
            /* ignore if the module included already in superclasses */
            for (p = RCLASS(klass)->super; p; p = RCLASS(p)->super) {
                switch (BUILTIN_TYPE(p)) {
                    case T_ICLASS:
                        if (RCLASS(p)->m_tbl == RCLASS(module)->m_tbl) {
                            if (!superclass_seen) {
                                c = p; /* move insertion point */
                            }
                           goto skip;
                        }
                        break;
                    case T_CLASS:
                        superclass_seen = Qtrue;
                        break;
                }
            }
            c = RCLASS(c)->super = include_class_new(module, RCLASS(c)->super);
            changed = 1;
        skip:
            module = RCLASS(module)->super;
        }
        if (changed) rb_clear_cache();
    }
    (class.c)

    经过这段代码的处理,会生成一个包含类(include class),它会加到原来的体系中,就像下面这个样子:
        super class
               |
        include class
               |
            klass


    包含类的iv_table和m_table就指向了模块的iv_table和m_table,通过这种方法,模块内的内容就可以在类中访问到了。

    我们注意到,这段代码并不像这里说得那么简单,至少还有两个循环在那里。先来看看外面的while循环,简化之:
    while (module) {
        ...
        module = RCLASS(module)->super;
    }

    这段代码告诉我们,我们是沿着模块的继承体系一路向上的……

    慢着,模块的继承体系?模块也有继承吗?至少在Ruby的层次上不记得有这种说法。事实上,在Ruby的层次上确实没有,你不会去让一个模块继承另一个模块。那么模块的继承体系从何谈起呢?

    我们知道,不仅是类可以包含模块,模块也可以包含模块。调用的当然也是这里的代码,由此可见,这个函数的第一个参数(klass)命名并不像看上那么完美。如果模块包含类模块,走这段代码,其结果就是这个样子:
         include module
                 |
            module

    所以,虽然在Ruby的层次上模块没有继承,但是在C的层次上,它也是可以有超类的。回到原来的路上,沿着模块的继承体系一路向上,就可以把一个模块所包含的所有模块都加入这个类的继承体系之中。

    再来看看内部的for循环,简化之:
    for (p = RCLASS(klass)->super; p; p = RCLASS(p)->super) {
        if (p已经包含过) {
            略过
        }
    }

    正如前面的注释所写,这段代码所要做的就是忽略已经包含的模块,这是一个经济实惠的选择。此外,这里还有一段代码:
    if (RCLASS(p)->m_tbl == RCLASS(module)->m_tbl) {
        if (!superclass_seen) {
            c = p;    /* move insertion point */
        }
        ...
    }

    注释告诉我们,这里移动了插入点,这段代码保证了插入包含类之后的结构依然能够保证各包含类之间顺序的一致性。

    谈及include_class_new时,我们曾经忽略了一段代码:
    if (TYPE(module) == T_ICLASS) {
        RBASIC(klass)->klass = RBASIC(module)->klass;
    }
    else {
        RBASIC(klass)->klass = module;
    }
    (class.c)

    这里,我们将新生成的包含类的klass字段设置为对应module,也就是建立起包含类与对应模块之间的联系。其实,这里设置klass多少有些偏离这个字段本身的含义,但对于包含类来说,把设置这个字段也算是“废物利用”,节省了一些空间。Module#ancestors方法(实现参见class.c的rb_mod_ancestors),正是利用了这个特点,去寻找一个模块的“祖先”。

  • Charles Oliver Nutter,JRuby的开发者,最近一直致力于解决JRuby臭名昭著的性能问题,在他的一篇blog中,他谈到了动态调用的问题:
    Making Dynamic Invocation Fast: An Idea?

    谈到方法调用的时候,我曾经解释过Ruby如何实现方法的动态调用,主要在于如何找到一个方法,也就是查找和缓存两步。为什么要缓存?主要是基于性能的考虑。

    即便同样采用了缓存技术,实现也是存在差别的。下面是JRuby中查找方法的实现:
    public ICallable searchMethod(String name) {
        for (RubyModule searchModule = this; searchModule != null; searchModule = searchModule.getSuperClass()) {
            synchronized(searchModule.getMethods()) {
                ICallable method = (ICallable) searchModule.getMethods().get(name);
                if (method != null) {
                    if (searchModule != this) {
                        addCachedMethod(name, method);
                    }

                    return method;
                }
            }
        }

        return UndefinedMethod.getInstance();
    }
    (RubyModule.java)

    这段代码基本上等同于之前介绍Ruby的对应实现,也就是说,会沿着类的继承体系一路向上,去寻找对应的方法。显然,JRuby的开发者也知道前面提及的性能问题,所以,当找到对应方法时,他们也做了一个缓存:
        addCachedMethod(name, method);

    这个方法的实现如下:
    private void addCachedMethod(String name, ICallable method) {
        if (!isIncluded()) {
            getMethods().put(name, method);
            getRuntime().getCacheMap().add(method, this);
        }
    }
    (RubyModule.java)

    这段代码告诉我们,所谓缓存,实际上就是放到了这个类(或模块)的方法表中,当再次调用方法时,就可以字节从这个类(或模块)本身找到这个方法,也就是一次调用,而不必再次沿着类的继承体系向上找寻了。

    初看上去,这个实现很不错,但对比于Ruby的实现便不难发现一些问题。首先,便是缓存的实现。来看看Ruby的缓存是如何实现的:
    #define CACHE_SIZE 0x800
    #define CACHE_MASK 0x7ff
    #define EXPR1(c,m) ((((c)>>3)^(m))&CACHE_MASK)

    struct cache_entry {  /* method hash table. */
        ID mid;   /* method's id */
        ID mid0;   /* method's original id */
        VALUE klass;  /* receiver's class */
        VALUE origin;  /* where method defined  */
        NODE *method;
        int noex;
    };

    static struct cache_entry cache[CACHE_SIZE];
    (eval.c)

    用法我们已经在《管窥Ruby——方法调用》中见识过了:
    ent = cache + EXPR1(klass, mid);

    这里,klass表示一个类,它是一个VALUE,也就是long。mid表示一个方法,对应于方法名,它是ID,实际上也是long。

    通过上面的代码,我们知道,cache实际上就是一个数组,查找缓存项的方法就是计算数组的索引,索引的计算方法已经摆在上面了,这样的计算显然比在Hash表中查找来得更加简单。对比来看,我们不难发现,这是JRuby性能还有提高空间的原因之一。

    再来看看原因之二。在JRuby中,用来作为查找的键值是什么?
    searchModule.getMethods().get(name);

    没错,字符串。这里查找的容器是Hash表。我们知道,Java的Hash表中计算键值需要用到hashCode这个方法:
    public int hashCode() {
        int h = hash;
        if (h == 0) {
            int off = offset;
            char val[] = value;
            int len = count;

            for (int i = 0; i < len; i++) {
                h = 31*h + val[off++];
            }
            hash = h;
        }
        return h;
    }
    (String.java)

    这个hashCode方法同字符串自身的长度有关,字符串越长计算的代价也就越高。显然,JDK的开发者也意识到了这个问题,所以,他们在实现的时候也采用的缓存的技术,将结果单独保存起来。不过,通过平摊成本来看,它同直接采用long相比还是有一些差距的。

    这里提到的JRuby版本是0.9.2,Charles Oliver Nutter如此积极的在解决Ruby的性能问题,相信JRuby这个低效的实现很快就会被改写。

    回到Charles Oliver Nutter的那篇blog,他在其中提到了一种巨型矩阵的方法,也就是把类和方法名作为两个维度,而把实际的方法作为对应的元素值。显然,这种做法存在巨大的缺陷,会造成巨大的空间浪费。作者也提到了这一点,所以,他提出以稀疏矩阵来解决问题。

    我不敢苟同这个实现,至少这种方式不比Ruby的实现来得更好。更何况将类和方法作为矩阵的二维,也要增加额外的转换成本。而且维护这个二维矩阵也是一件劳心费力的事情,不要忘了,动态语言的动态特性,增删方法也是允许的。再者,即便采用稀疏矩阵,查找也不见得来得更快。

    其实,这种想法本质上与Ruby的实现异曲同工,只是对比之下,显得有些笨拙。在Ruby中,通过前面提到的计算索引的方法,巧妙的将两个维度合并为一个维度。Ruby的实现更多在体现缓存的思想,而按照Charles Oliver Nutter的设想,方法表就会失去其价值,因为在这个想法中,矩阵存储了所有的方法。

    这篇blog引出了一些很有意思的评论,沿着这些路走出去,还是会有一些不同的发现。

  • 周末,我在校验《Ruby Hacking Guide》(简称RHG)。

    RHG是一本剖析Ruby源码的书。其实,这是日文书,原名是《Ruby源码完全解说》,作者是青木峰郎。很早就听说过这本书。关于操作系统源码剖析的书现在已经有很多了,但是关于程序设计语言源码剖析的书少之又少,所以,这本书引起了我的兴趣。不过,这是一本日文书,最终我也就没动看它的念头。之前一段时间,开始真正接触Ruby。习惯使然,我下载编译了Ruby的源码,本想更深入的了解一下,却不知道从何下手。就在这时,我发现RHG这个项目,它要把这本日文书译成英文。找到这个项目时,它已经完成了第一次发布,其中包括了第二章《对象》、第三章《名称与名称表》、第四章《类与模块》和第六章《变量和常量》,这四章刚好讲解了Ruby的Runtime结构。我的Ruby源码之旅就是从这里起步的。

    开始阅读之后没多长时间,我突发奇想,决定翻译这本书,反正阅读过程中也要做一些笔记,索性翻译过来,也许对人对己都有好处。在这个念头的驱动之下,我开始自己第一次的翻译之旅。看过不少译文的东西,也曾经尝试过翻译一些短小的东西,我知道翻译这活不好干,明明看懂的东西,说出来就不像中国话,加上英文也是翻译版本,其中某些部分的翻译也让人拿捏不准,所以常常需要借助翻译软件去翻译日文,然后再来理解。翻译过程中,有很多次我都想放弃,但最后还是咬牙坚持下来,用了两三个星期的业余时间,终于完成了初稿的翻译。我清楚,第一遍完成的东西没法见人,准备校验之后再发布。结果,这一扔就是几个月。

    因为精力转移到其它事情上,初稿就一直在自己的机器上躺着。其间,我也把初稿发给过一些朋友,希望他们可以帮我校验,最终,只得到很少的反馈。这几个月里,我读了许多Ruby的源码,对Ruby理解增进了不少,也把记录了自己的一些心得,不知不觉中写了个《管窥Ruby》的系列出来,中间出过一个小笑话,我在railscn上发布过我在翻译RHG的消息,有人莫明其妙的夸了我一次,说我翻译的RHG不错,我疑惑,不记得我发布过自己翻译的版本,原来他误以为《管窥Ruby》就是RHG。我承认我读Ruby源码是从RHG起步,但《管窥Ruby》绝对属于我的原创,有些内容中我还没在RHG读到,比如Allocator。^_^

    其实,不仅仅是我的译稿进度缓慢,RHG这个项目也是同样,英文版的SVN上已经好久没有更新了。

    最近在网上忽然发现了一个blog,其主人要翻译RHG。于是,我想起了自己机器上睡着的初稿。我赶紧与他取得联系,希望把我们翻译的部分合并。这个朋友懂得日文,他想从日文直接翻译成中文。这就太棒了,不必再等RHG项目缓慢的动作了。通过讨论,我们达成一致,把彼此翻译的部分合并,争取在年底之前,发布第一部分,也就是第一章到第七章。按照他的说法,他完成了前言和第一章,我已经有了二、三、四和六章,第七章已经有了部分英文版本,而且篇幅不大,所以,主要的工作在第五章,关于垃圾回收的章节。

    周末,翻出闲置已久的译稿开始校验工作,发现了大量错别字和语句不通顺的地方。原来,校验也不轻松多少,好多内容还要重新理解。不过,经过几个月的积累,我发觉自己已经具备了技术校验的能力,原来一些翻译之时不知所谓的内容,现在也能够理解了。我不敢说校验之后的版本就很好了,至少已经比较符合我个人的阅读习惯了,当然,还有少许我无法校准的地方。

    如果你懂日语,或是能够做校验的工作,希望你可以贡献自己的力量,欢迎与我们联系!