本人水平有限,如有错误,欢迎指正与补充。
ruby 3.2 除了 YJIT,还有一些别的小优化,例如也是来自 shopify 的 Object Shapes。其作用有提高缓存命中率,加速实例变量 (和别的属性) 的读写。
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
,而不用顺着类的继承链找。
这棵树的另一个好处是,可以提高缓存命中率。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 等好处,详情见下面的帖子和视频演讲