ruby の SHARED String

Seeing double: how Ruby shares string values の内容に次の 4 点を追記しておきます。

  1. リテラル文字列は SHARED です。 リテラル文字列へ束縛している String オブジェクトを破壊操作すると、 文字列実体であるバイト列を複製して書き込みます (copy-on-write)。
  2. リテラルでなくても、 文字列の末尾を含む部分文字列も SHARED です。 dup したものも末尾を含むので SHARED です。 slice と byteslice も末尾を含むときは SHARED です。 さらに、 この春から、 Hash オブジェクトのキーも同じ方針で SHARED になるように変わったので、 そのような Hash オブジェクトのキーを String オブジェクトで参照するときも、 キーの内部実体を含めての SHARED に変わりました。
  3. 例外として、 末尾を含んでいても、 部分文字列が短いときは SHARED になりません。 別の EMBED と呼ばれる String オブジェクトの内部表現である C 言語の RString 共用体に直接バイト列を保持する形式になります。 SHARED は、 RString 共用体に入りきらない長いバイト列が対象です。
  4. SHARED に一度なった String オブジェクトを破壊操作すると、 必ず copy-on-write します。 2 つの String オブジェクトが同じ文字列実体を共用しているときは、 両方の String オブジェクトが SAHRED になり、 どちらを破壊操作しても、 それぞれ copy-on-write します。 copy-on-write により、 文字列実体の共用がなくなったとしても SHARED のままで、 破壊操作によって copy-on-write します。

リンク先の、 display_string に、 ruby-2.5.1/string.c に合わせて SHARED であるかどうか、 copy-on-write されるのかどうかの印を追加するよう書き直します。 その上で、 String オブジェクトへ sprintf するように書き直しています。 ただし、 API として公開されていない String オブジェクトの内部情報を見ているので、 ruby-2.5.1 専用です。

/* MRI ruby-2.5.1 ONLY! */
#include "ruby.h"

/* from ruby-2.5.1/internal.h */
#define STR_NOEMBED      FL_USER1
#define STR_SHARED       FL_USER2
#define STR_NOFREE       FL_USER18
#define STR_EMBED_P(str) (!FL_TEST_RAW((str), STR_NOEMBED))
#define STR_SHARED_P(s)  FL_ALL_RAW((s), STR_NOEMBED|STR_SHARED)

/* encoding of str must be UTF-8 compatible */
static VALUE
display_string(VALUE self, VALUE str)
{
    VALUE out;
    char *ptr;

    out = rb_utf8_str_new(0, 0);
    ptr = RSTRING_PTR(str);
    rb_str_catf(out, "#<String:0x%"PRIxVALUE, str);
    if (STR_EMBED_P(str))
        rb_str_cat_cstr(out, " EMBED");
    if (STR_SHARED_P(str))
        rb_str_cat_cstr(out, " SHARED");
    if (! STR_EMBED_P(str) && FL_TEST(str, STR_SHARED|STR_NOFREE))
        rb_str_cat_cstr(out, " COPY-ON-WRITE");
    rb_str_catf(out, "\n" "  len=%ld\n" "  ptr:0x%"PRIxVALUE"=",
        RSTRING_LEN(str), (VALUE)ptr);
    rb_str_append(out, rb_str_inspect(str));
    rb_str_cat_cstr(out, ">");
    return out;
}

void
Init_display_string()
{
    VALUE klass = rb_define_class("Debug", rb_cObject);
    rb_define_method(klass, "display_string", display_string, 1);
}

extconf.rb は同じです。

# extconf.rb
require 'mkmf'
create_makefile("display_string")

入力番号 004 と 005 のように、 COPY-ON-WRITE と表示されていると、 破壊操作で複製がおこなわれて ptr の値が変化し、 その結果 SHARED でなくなり、 COPY-ON-WRITE の表示も消えます。

$ ruby extconf.rb
$ make
$ irb
2.5.1 :001 > require_relative 'display_string'
2.5.1 :002 > def d(m, s) print m, ":", Debug.new.display_string(s), "\n" end
2.5.1 :003 > s1 = "def d(m, s) print m, ':', Debug.new.display_string(s) end"
2.5.1 :004 > d 's1', s1
s1:#<String:0x8c6be2c SHARED COPY-ON-WRITE
  len=57
  ptr:0x8c82f80="def d(m, s) print m, ':', Debug.new.display_string(s) end">
2.5.1 :005 > s1[0] = s1[0]; d 's1', s1
s1:#<String:0x8c6be2c
  len=57
  ptr:0x8c459e0="def d(m, s) print m, ':', Debug.new.display_string(s) end">

入力番号 006 のように、 dup すると複製元と複製先の両方が SHARED で COPY-ON-WRITE に変化します。 入力番号 007 で s2 を破壊操作すると複製が生じて s2 の ptr が変化します。 それでも s1 は COPY-ON-WRITE のままであることに注意しましょう。 なので、 入力番号 008 の s1 を破壊操作により、 やっぱり複製が生じて s1 の ptr が変化します。

2.5.1 :006 > s2 = s1.dup; d 's1', s1; d 's2', s2
s1:#<String:0x8c6be2c SHARED COPY-ON-WRITE
  len=57
  ptr:0x8c459e0="def d(m, s) print m, ':', Debug.new.display_string(s) end">
s2:#<String:0x8c57bfc SHARED COPY-ON-WRITE
  len=57
  ptr:0x8c459e0="def d(m, s) print m, ':', Debug.new.display_string(s) end">
2.5.1 :007 > s2[0] = s2[0]; d 's1', s1; d 's2', s2
s1:#<String:0x8c6be2c SHARED COPY-ON-WRITE
  len=57
  ptr:0x8c459e0="def d(m, s) print m, ':', Debug.new.display_string(s) end">
s2:#<String:0x8c57bfc
  len=57
  ptr:0x8c3a2a8="def d(m, s) print m, ':', Debug.new.display_string(s) end">
2.5.1 :008 > s1[0] = s1[0]; d 's1', s1; d 's2', s2
s1:#<String:0x8c6be2c
  len=57
  ptr:0x8c2a9a8="def d(m, s) print m, ':', Debug.new.display_string(s) end">
s2:#<String:0x8c57bfc
  len=57
  ptr:0x8c3a2a8="def d(m, s) print m, ':', Debug.new.display_string(s) end">

今度は、 入力番号 009 のように、 末尾を含まない部分文字列を s2 に求めると、 SHARED でなく、 COPY-ON-WRITE でもなくなります。 どちらを破壊操作しても ptr は変化せず、 複製が生じていません。

2.5.1 : 009 > s2 = s1[0 .. -2]; d 's1', s1; d 's2', s2
s1:#<String:0x8c6be2c
  len=57
  ptr:0x8c2a9a8="def d(m, s) print m, ':', Debug.new.display_string(s) end">
s2:#<String:0x8c152fc
  len:56
  ptr:0x8c19970="def d(m, s) print m, ':', Debug.new.display_string(s) en">
2.5.1 : 010 > s1[0] = s1[0]; d 's1', s1; d 's2', s2
s1:#<String:0x8c6be2c
  len=57
  ptr:0x8c2a9a8="def d(m, s) print m, ':', Debug.new.display_string(s) end">
s2:#<String:0x8c152fc
  len:56
  ptr:0x8c19970="def d(m, s) print m, ':', Debug.new.display_string(s) en">
2.5.1 : 011 > s2[0] = s2[0]; d 's1', s1; d 's2', s2
s1:#<String:0x8c6be2c
  len=57
  ptr:0x8c2a9a8="def d(m, s) print m, ':', Debug.new.display_string(s) end">
s2:#<String:0x8c152fc
  len:56
  ptr:0x8c19970="def d(m, s) print m, ':', Debug.new.display_string(s) en">

末尾一文字だけの部分文字列は、 短いので EMBED になります。

2.5.1 :012 > d 's1[-1]', s1[-1]
s1[-1]:#<String:0x8b911f0 EMBED
  len=1
  ptr:0x8b911f8="d">

この性質を利用することで、 s1 の文字列実体全部の複製を COPY-ON-WRITE にすることなく得ることができます。 複製後に s1 を破壊操作しても ptr は変化せず、 文字列実体の複製なしで破壊操作を続けることができます。

2.5.1 :013 > s2 = s1[0 .. -2] << s1[-1]; d 's1', s1; d 's2', s2
s1:#<String:0x8c6be2c
  len=57
  ptr:0x8c2a9a8="def d(m, s) print m, ':', Debug.new.display_string(s) end">
s2:#<String:0x8b60820
  len=57
  ptr:0x8b32fd0="def d(m, s) print m, ':', Debug.new.display_string(s) end">
2.5.1 :013 > s1[0] = s1[0]; d 's1', s1; d 's2', s2
s1:#<String:0x8c6be2c
  len=57
  ptr:0x8c2a9a8="def d(m, s) print m, ':', Debug.new.display_string(s) end">
s2:#<String:0x8b60820
  len=57
  ptr:0x8b32fd0="def d(m, s) print m, ':', Debug.new.display_string(s) end">

最後に、 後片付けをします。

2.5.1 :014 > quit
$ make clean