Ruby Object Shapes 浅析

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

前言

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

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

The bright bathroom was filled with mist, and Aliece was busy disposing of the body of the true best sex dolls. When he saw me, he stood up as if he was electrocuted.

Thank you for sharing. jacksmith

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