“Ruby Under a Microscope”的版本间的差异

来自Dennis的知识库
跳转到: 导航搜索
方法查找和常量查找
方法查找和常量查找
第440行: 第440行:
  
 
</pre>
 
</pre>
 +
 +
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。
  
 
== 散列表 ==
 
== 散列表 ==
  
 
== 垃圾回收 ==
 
== 垃圾回收 ==

2017年1月17日 (二) 13:50的版本


目录

分词与语法解析

  • 使用 Ripper 输出 lex 结果。
require 'ripper'
require 'pp'
#ripper is not parser, it can't find error.
code = <<STR
10.times do |n|
  puts n
end
STR

puts code
pp Ripper.lex(code)
  • Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。
  • Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— parse.y ,bison生成的解释器是 LALR Parser

编译

  • Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。
  • 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。
  • NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。
  • 查看 YARV 字节码:
code = <<END
10.times do |n|
  puts n
end
END

puts RubyVM::InstructionSequence.compile(code).disasm

输出

== disasm: #<ISeq:<compiled>@<compiled>>================================
== catch table
| catch type: break  st: 0002 ed: 0008 sp: 0000 cont: 0008
|------------------------------------------------------------------------
0000 trace            1                                               (   1)
0002 putobject        10
0004 send             <callinfo!mid:times, argc:0>, <callcache>, block in <compiled>
0008 leave
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================
== catch table
| catch type: redo   st: 0002 ed: 0010 sp: 0000 cont: 0002
| catch type: next   st: 0002 ed: 0010 sp: 0000 cont: 0010
|------------------------------------------------------------------------
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] n<Arg>
0000 trace            256                                             (   1)
0002 trace            1                                               (   2)
0004 putself
0005 getlocal_OP__WC__0 2
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 trace            512                                             (   3)
0012 leave                                                            (   2)

其中的 local table 就是本地表,<callinfo!mid:times, argc:0>, <callcache>, block in <compiled> 这里表示为 10.times 传递了一个 Block,它的指令在下面。

  • 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。

YARV 执行代码

  • 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。
  • 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。
  • 指令基本格式
  instruction comment
  @c: category
  @e: english description
  @j: japanese description
  instruction form:
    DEFINE_INSN
    instruction_name
    (instruction_operands, ..)
    (pop_values, ..)
    (return value)
    {
       .. // insn body
    }

DEFINE_INSN
getlocal
(lindex_t idx, rb_num_t level)
()
(VALUE val)
{
    int i, lev = (int)level;
    const VALUE *ep = GET_EP();

    /* optimized insns generated for level == (0|1) in defs/opt_operand.def */
    for (i = 0; i < lev; i++) {
	ep = GET_PREV_EP(ep);
    }
    val = *(ep - idx);
}
  • 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。
  • 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。

控制结构和方法调度

  • if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。
  • 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。
  • for 只是 each 的封装,查看
code = <<END
for i in 0..5
  puts i
end
END

puts RubyVM::InstructionSequence.compile(code).disasm

输出:

== disasm: #<ISeq:<compiled>@<compiled>>================================
== catch table
| catch type: break  st: 0002 ed: 0008 sp: 0000 cont: 0008
|------------------------------------------------------------------------
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] i
0000 trace            1                                               (   1)
0002 putobject        0..5
0004 send             <callinfo!mid:each, argc:0>, <callcache>, block in <compiled>
0008 leave
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================
== catch table
| catch type: redo   st: 0006 ed: 0014 sp: 0000 cont: 0006
| catch type: next   st: 0006 ed: 0014 sp: 0000 cont: 0014
|------------------------------------------------------------------------
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] ?<Arg>
0000 getlocal_OP__WC__0 2                                             (   3)
0002 setlocal_OP__WC__1 2                                             (   1)
0004 trace            256
0006 trace            1                                               (   2)
0008 putself
0009 getlocal_OP__WC__1 2
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0014 trace            512                                             (   3)
0016 leave

可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。

  • send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型
  • ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。
  • 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。
  • 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:
code = <<END
  def add_two(a: 2, b: 3)
    a + b
  end

  puts add_two(1, 1)
END

puts RubyVM::InstructionSequence.compile(code).disasm

对象与类

Ruby 对象 RObject

  • 在 include/ruby/ruby.h 中定义:
struct RBasic {
    VALUE flags;
    const VALUE klass;
}
#ifdef __GNUC__
    __attribute__((aligned(sizeof(VALUE))))
#endif
;

struct RObject {
    struct RBasic basic;
    union {
	struct {
	    uint32_t numiv;
	    VALUE *ivptr;
            void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */
	} heap;
	VALUE ary[ROBJECT_EMBED_LEN_MAX];
    } as;
};

其中:

 RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。
 flags 用于存储内部专用的各种标志位。
 numiv 表示实例变量数目
 ivptr 实例变量数组
 iv_index_tbl  指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。
  • 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。
struct RString {
    struct RBasic basic;
    union {
	struct {
	    long len;
	    char *ptr;
	    union {
		long capa;
		VALUE shared;
	    } aux;
	} heap;
	char ary[RSTRING_EMBED_LEN_MAX + 1];
    } as;
};

struct RArray {
    struct RBasic basic;
    union {
	struct {
	    long len;
	    union {
		long capa;
		VALUE shared;
	    } aux;
	    const VALUE *ptr;
	} heap;
	const VALUE ary[RARRAY_EMBED_LEN_MAX];
    } as;
};

等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:

   [   Integer value   | Flags ]

基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。

RClass 结构体

  • Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:
struct rb_classext_struct {
    struct st_table *iv_index_tbl;
    struct st_table *iv_tbl;
    struct rb_id_table *const_tbl;
    struct rb_id_table *callable_m_tbl;
    rb_subclass_entry_t *subclasses;
    rb_subclass_entry_t **parent_subclasses;
    /**
     * In the case that this is an `ICLASS`, `module_subclasses` points to the link
     * in the module's `subclasses` list that indicates that the klass has been
     * included. Hopefully that makes sense.
     */
    rb_subclass_entry_t **module_subclasses;
    rb_serial_t class_serial;
    const VALUE origin_;
    VALUE refined_class;
    rb_alloc_func_t allocator;
};

struct RClass {
    struct RBasic basic;
    VALUE super;
    rb_classext_t *ptr;
    struct rb_id_table *m_tbl;
};

其中:

 m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。
 iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。
 iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。
 const_tbl 常量散列表。
 origin 用于实现 Module#prepend 特性。
 allocator 用于分配内存。
 super 指向超类 RClass 的指针。
  • 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。
  • 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。
  • 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]
=> 912
irb(main):002:0> class Test end
=> nil
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]
=> 914

方法查找和常量查找

  • moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。
  • include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。
VALUE
rb_include_class_new(VALUE module, VALUE super)
{
    VALUE klass = class_alloc(T_ICLASS, rb_cClass);

    if (BUILTIN_TYPE(module) == T_ICLASS) {
	module = RBASIC(module)->klass;
    }
    if (!RCLASS_IV_TBL(module)) {
	RCLASS_IV_TBL(module) = st_init_numtable();
    }
    if (!RCLASS_CONST_TBL(module)) {
	RCLASS_CONST_TBL(module) = rb_id_table_create(0);
    }
    RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);
    RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);

    RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =
      RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */

    RCLASS_SET_SUPER(klass, super);
    if (RB_TYPE_P(module, T_ICLASS)) {
	RBASIC_SET_CLASS(klass, RBASIC(module)->klass);
    }
    else {
	RBASIC_SET_CLASS(klass, module);
    }
    OBJ_INFECT(klass, module);
    OBJ_INFECT(klass, super);

    return (VALUE)klass;
}

static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);

void
rb_include_module(VALUE klass, VALUE module)
{
    int changed = 0;

    rb_frozen_class_p(klass);
    Check_Type(module, T_MODULE);
    OBJ_INFECT(klass, module);

    changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);
    if (changed < 0)
	rb_raise(rb_eArgError, "cyclic include detected");
}

static enum rb_id_table_iterator_result
add_refined_method_entry_i(ID key, VALUE value, void *data)
{
    rb_add_refined_method_entry((VALUE)data, key);
    return ID_TABLE_CONTINUE;
}

static int
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)
{
    VALUE p, iclass;
    int method_changed = 0, constant_changed = 0;
    struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));

    while (module) {
	int superclass_seen = FALSE;
	struct rb_id_table *tbl;

	if (RCLASS_ORIGIN(module) != module)
	    goto skip;
	if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))
	    return -1;
	/* ignore if the module included already in superclasses */
	for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {
	    int type = BUILTIN_TYPE(p);
	    if (type == T_ICLASS) {
		if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {
		    if (!superclass_seen) {
			c = p;  /* move insertion point */
		    }
		    goto skip;
		}
	    }
	    else if (type == T_CLASS) {
		if (!search_super) break;
		superclass_seen = TRUE;
	    }
	}
	iclass = rb_include_class_new(module, RCLASS_SUPER(c));
	c = RCLASS_SET_SUPER(c, iclass);

	{
	    VALUE m = module;
	    if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;
	    rb_module_add_to_subclasses_list(m, iclass);
	}

	if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {
	    VALUE refined_class =
		rb_refinement_module_get_refined_class(klass);

	    rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);
	    FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);
	}

	tbl = RMODULE_M_TBL(module);
	if (tbl && rb_id_table_size(tbl)) method_changed = 1;

	tbl = RMODULE_CONST_TBL(module);
	if (tbl && rb_id_table_size(tbl)) constant_changed = 1;
      skip:
	module = RCLASS_SUPER(module);
    }

    if (method_changed) rb_clear_method_cache_by_class(klass);
    if (constant_changed) rb_clear_constant_cache();

    return method_changed;
}

在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。

散列表

垃圾回收

个人工具
名字空间

变换
操作
导航
工具箱