Ruby Object Shapes 浅析

zhuoerri · 2024年02月03日 · 最后由 Camilayost 回复于 2024年03月09日 · 665 次阅读

前言

本人水平有限,如有错误,欢迎指正与补充。

ruby 3.2 除了 YJIT,还有一些别的小优化,例如也是来自 shopify 的 Object Shapes。其作用有提高缓存命中率,加速实例变量 (和别的属性) 的读写。

Shape Tree

Object Shapes 的思路是设计一个全局的前缀树 Shape Tree, 每新增一个实例变量时,就增加一个节点。如下 foo 的形状 id 是 2, bar 的形状 id 是 4

class Foo
  def initialize
    # Currently this instance is the root shape (ID 0)
    @a = 1 # Transitions to a new shape via edge @a (ID 1)
    @b = 2 # Transitions to a new shape via edge @b (ID 2)
  end
end

class Bar
  def initialize
    # Currently this instance is the root shape (ID 0)
    @a = 1 # Transitions to shape defined earlier via edge @a (ID 1)
    @c = 1 # Transitions to a new shape via edge @c (ID 3)
    @b = 1 # Transitions to a new shape via edge @b (ID 4)
  end
end

foo = Foo.new # blue in the diagram
bar = Bar.new # red in the diagram

这样的好处是,例如根据这棵树,就可以知道 foo 的@b实例变量是在第二的位置,直接读取 foo 的第二个实例变量就能读取到@b,而不用顺着类的继承链找。

Inline Cache

这棵树的另一个好处是,可以提高缓存命中率。ruby 写入实例变量的字节码 setinstancevariable,有一个内联缓存 inline cache, 例如下图的<is:0>, <is:1>

def a
  @a = 0
  @b = 0
end
puts RubyVM::InstructionSequence.of(method(:a)).disasm

== disasm: #<ISeq:a@(irb):17 (17,0)-(20,3)> (catch: FALSE)
0000 putobject_INT2FIX_0_                                             (  18)[LiCa]
0001 setinstancevariable                    :@a, <is:0>
0004 putobject_INT2FIX_0_                                             (  19)[Li]
0005 dup
0006 setinstancevariable                    :@b, <is:1>
0009 leave                                                            (  20)[Re]
=> nil

ruby 3.2 前的实例变量的缓存逻辑是根据类,如果子类继承的话,会让缓存无效。但现在根据前缀树 Shape Tree, 则下面的 hoge 和 fuga 是相同的形状 id, initialize 方法里能让缓存命中

class Hoge
  def initialize
    # fuga初始状态,形状id为0
    @a = 0 # <is:0>缓存里,记录了上次hoge的{ 形状id为0 => 位置1 }, 命中缓存,从而得知@a在第一个的位置, 然后fuga形状id变为1
    @b = 0 # <is:1>缓存里,记录了上次hoge的{ 形状id为1 => 位置2 }, 命中缓存,从而得知@b在第二个的位置,然后fuga形状id变为2
  end
end

class Fuga < Hoge; end

hoge = Hoge.new
fuga = Fuga.new

红黑树

为了防止实例变量太多,前缀树太深,不好查找。2023 年,tenderlove 加入了红黑树,当 Shape Tree 深度达到阈值后,新建一颗对应的红黑树来加速查找

简略后源码如下:

/* https://github.com/ruby/ruby/blob/5124f9ac7513eb590c37717337c430cb93caa151/vm_insnhelper.c#L1616C1-L1641C2 */

/* 设置实例变量的字节码对应的函数 */
static inline void
vm_setinstancevariable(const rb_iseq_t *iseq, VALUE obj, ID id, VALUE val, IVC ic)
{
    shape_id_t dest_shape_id;
    attr_index_t index;
    vm_ic_atomic_shape_and_index(ic, &dest_shape_id, &index); /* 从inline cache缓存中读取形状id和实例变量位置index */

    if (UNLIKELY(UNDEF_P(vm_setivar(obj, id, val, dest_shape_id, index)))) { /* vm_setivar会检查缓存的dest_shape_id是否匹配当前对象obj的形状id */
        vm_setivar_slowpath_ivar(obj, id, val, iseq, ic); /* 如果inline cache缓存没命中,则还是顺着继承链找实例变量位置, 把位置写入新Shape Tree节点。同时写入inline cache缓存,供下次尝试匹配 */
    }
}


/* github.com/ruby/ruby/blob/5124f9ac7513eb590c37717337c430cb93caa151/shape.c#L459C1-L487C2 */

/* Shape Tree 创建子节点的函数 */
static rb_shape_t *
rb_shape_alloc_new_child(ID id, rb_shape_t * shape, enum shape_type shape_type)
{
    rb_shape_t * new_shape = rb_shape_alloc(id, shape, shape_type); /* 新建节点 */

    switch (shape_type) {
      case SHAPE_IVAR: /* 如果是用于实例变量的节点 */
        new_shape->next_iv_index = shape->next_iv_index + 1; /* 新节点的实例变量位置是父节点位置 + 1 */
        if (new_shape->next_iv_index > ANCESTOR_CACHE_THRESHOLD) {
            redblack_cache_ancestors(new_shape); /* 如果深度超过阈值,新建一个红黑树来加速查找 */
        }
        break;
      case SHAPE_FROZEN:
      case SHAPE_T_OBJECT:
        new_shape->next_iv_index = shape->next_iv_index;
        break;
      case SHAPE_OBJ_TOO_COMPLEX:
      case SHAPE_ROOT:
        rb_bug("Unreachable");
        break;
    }

    return new_shape;
}

参考资料

本文只简单讲了缓存命中率,Object Shapes 还有精简代码,有利 jit 等好处,详情见下面的帖子和视频演讲

https://bugs.ruby-lang.org/issues/18776

https://rubykaigi.org/2022/presentations/jemmaissroff.html

需要 登录 后方可回复, 如果你还没有账号请 注册新账号