• 2006-11-21

    管窥Ruby——方法调用

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://www.blogbus.com/dreamhead-logs/3877610.html

    在进入正题之前,先声明一下,这里讨论的是方法调用的结构,而非方法执行的过程。

    我一直觉得了解Ruby内部结构的一个很好的入口点是Ruby的API,讨论方法定义的时候,我们便是从Ruby API起步。同样,这次先来看看方法调用的Ruby API。在方法调用这个问题上,Ruby显得特别的大方,一口气提供了四个API:
    VALUE rb_funcall( VALUE recv, ID id, int argc, ... );
    VALUE rb_funcall2( VALUE recv, ID id, int argc, VALUE *args );
    VALUE rb_funcall3( VALUE recv, ID id, int argc, VALUE *args );
    VALUE rb_apply( VALUE recv, ID name, int argc, VALUE args );

    其中,recv相当于this指针。id指定了调用的方法,因为在Ruby中,字符串和id的一一对应关系,这就相当于用一个字符串指定了一个方法名。之后就是参数了,argc表示参数的个数,args表示实际的参数,这种手法在C编程中太常用了。这四个API的差别也就在参数上。

    第一个和第二个API实际上是一样的,差别是在于第一个API运用了可变长度参数,简化了代码编写。第三个API不能调用private方法。第四个API允许以Ruby的数组作为参数。

    Ruby的方法调用分了两个层次,这里的列出的API是最上面的一个层次,作为接口提供出来,而真正的调用过程是由rb_call完成,下面是rb_funcall2的代码:
    VALUE
    rb_funcall2(VALUE recv, ID mid, int argc, const VALUE *argv)
    {
        return rb_call(CLASS_OF(recv), recv, mid, argc, argv, NOEX_NOSUPER);
    }

    有了这样的区分就可以背着广大的API使用者,偷偷的在内部提供更细的控制,事实上,也是这样,rb_call在Ruby解释器的内部还是得到了广泛的采用。下面是rb_call的代码:

    static VALUE
    rb_call(klass, recv, mid, argc, argv, scope)
        VALUE klass, recv;
        ID    mid;
        int argc;   /* OK */
        const VALUE *argv;  /* OK */
        int scope;
    {
        NODE  *body;  /* OK */
        int    noex;
        ID     id = mid;
        struct cache_entry *ent;

        if (!klass) {
            rb_raise(rb_eNotImpError, "method `%s' called on terminated object (0x%lx)",
                rb_id2name(mid), recv);
        }
        /* is it in the method cache? */
        ent = cache + EXPR1(klass, mid);
        if (ent->mid == mid && ent->klass == klass) {
            if (!ent->method)
                 return method_missing(recv, mid, argc, argv, scope==2?CSTAT_VCALL:0);
            klass = ent->origin;
            id    = ent->mid0;
            noex  = ent->noex;
            body  = ent->method;
        }
        else if ((body = rb_get_method_body(&klass, &id, &noex)) == 0) {
            if (scope == 3) {
                return method_missing(recv, mid, argc, argv, CSTAT_SUPER);
            }
            return method_missing(recv, mid, argc, argv, scope==2?CSTAT_VCALL:0);
        }

        if (mid != missing && scope == 0) {
            /* receiver specified form for private method */
            if (noex & NOEX_PRIVATE)
                return method_missing(recv, mid, argc, argv, CSTAT_PRIV);

            /* self must be kind of a specified form for protected method */
            if (noex & NOEX_PROTECTED) {
                VALUE defined_class = klass;

                if (TYPE(defined_class) == T_ICLASS) {
                    defined_class = RBASIC(defined_class)->klass;
                }
                if (!rb_obj_is_kind_of(ruby_frame->self, rb_class_real(defined_class)))
                    return method_missing(recv, mid, argc, argv, CSTAT_PROT);
                }
        }

        return rb_call0(klass, recv, mid, id, argc, argv, body, noex);
    }
    (eval.c)

    抛开基本的检查代码,这段代码做了几件事:
    1 查找方法
    2 校验方法
    3 执行方法

    先来看看查找方法,谈到类的方法时,我们知道,方法在类中是以hash表的方式存放,所以,一般会有一个查表的过程,查找的代码在那篇blog中已经展示过了。

    在这里,查找代码首先做的并不是在hash表中查找,而是在一个方法缓存中查找:
    ent = cache + EXPR1(klass, mid);

    通常讨论hash表的时候,我们会把它看作是一个O(1)的操作,相比于很多高复杂度的算法来说,它是可以接受的。但是,对于方法调用这个在Ruby中很常见的操作来说,需要在O(1)的系数一级进行优化以提高整体的性能。如果一个方法如果定义在超类中,那么每次调用都执行循环来找,最差情况就是定义在类层次的根部,除此之外,hash本身还要有不少的运算负担。这里采用了方法缓存,只进行简单的运算,就可以把已经调用过的方法找到,这使得再次调用方法的负担大幅度减小。缓存技术是一项伟大的技术,在计算机的各个领域得到了广泛的应用,比如CPU的多级缓存,比如OS中虚拟内存。

    一个朋友实现一个类似的结构,不过,没有实现缓存,而且搜索方法没有采用循环,而是采用了递归。另外一个朋友做了一个测试,代码是Fibonacci数列,而且采用的是递归的方式,也就是可以导致复杂度非常高的那种:
        F(n) = F(n-1) + F(n-2)
    结果稍微大一点的数就要求用上很长一段时间,这是一个不错的例子,各个方面不是最优的选择。

    如果找到了这个方法,就会进行一些校验,看看调用端是否具备合法调用方法的资格。对比一下前面几个API的实现不难发现,这里提供了更细的设置,这些参数会在Ruby求值的过程中用到,所以,前面说前面说它们背着广大的API使用者。

    至于方法的执行过程,开篇就说过了,这里不讨论了。

    分享到:

    历史上的今天:

    买书不读 2005-11-21
    引用地址: