String.new の :capacity の説明が合っていない?
elfham opened this issue · comments
elfham commented
String.new
の :capacity
の説明ですが、
内部バッファのサイズを指定します。指定することで、なんども文字列連結する (そしてreallocがなんども呼ばれる)ときのパフォーマンスが改善されるかもしれません。省略した場合、引数stringのバイト数が127未満であれば127、それ以上であればstring.bytesizeになります。
以前のバージョンでの仕様は分からないのですが、 3.1.0 以降では、省略時のデフォルトの説明が実際の動作と合っていないように思われます。
:capacity
は RString
の as.heap.aux.capa
に相当するかと思いますが、実際には以下のような動作になるようです。
:capacity
の指定の無い場合- 文字列のサイズが小さい場合
- 文字列は
RString
に embed されるので capa を持たない
- 文字列は
- 文字列のサイズが大きい (※) 場合
- 文字列は別の freeze された文字列を参照して共有しているので、当該文字列オブジェクト自体は capa を持たない
- 文字列のサイズが小さい場合
:capacity
を指定した場合- 62 未満の場合
as.heap.aux.capa
は 63 になる
- 63 以上の場合
as.heap.aux.capa
は指定した値になる
- 62 未満の場合
※ 3.1.0 (USE_RVARGC 未指定) では 24 バイトから、 3.2.0 では 16 バイトから
説明のうまい修正案がちょっと思い付きませんが、「省略した場合」以降は削ってしまっても良いように思います。
メソッドシグネチャーの
new(string = "") -> String
new(string = "", encoding: string.encoding, capacity: 63) -> String
new(string = "", encoding: string.encoding, capacity: string.bytesize) -> String
も悩ましいですが、少なくとも string.bytesize
になることは無いようなので 3 つ目は無くて良いように思います。
確認に使用したスクリプトと実行結果を以下に示します。
require 'inline'
class String
def super_inspect
self.class.superclass.instance_method(:inspect).bind(self).call
end
raise NotImplementedError, 'Ruby 3.1.0 required' if RUBY_VERSION < '3.1.0'
inline do |builder|
builder.include '<stdio.h>'
builder.add_compile_flags '-Wall'
builder.c_raw <<~CODE
VALUE inspect_rstring(int argc, VALUE *argv, VALUE self) {
FILE *o = stdout;
if (argc == 1) {
/* Output Title */
if (TYPE(argv[0]) != T_STRING)
rb_raise(rb_eTypeError, "Title Not String");
fprintf(o, "%s\\n", RSTRING_PTR(argv[0]));
}
char *rstring_ptr = RSTRING_PTR(self);
long rstring_len = RSTRING_LEN(self);
bool str_embed = ! (RBASIC(self)->flags & RSTRING_NOEMBED);
bool str_shared = RBASIC(self)->flags & ELTS_SHARED;
struct RString *rstring = RSTRING(self);
fprintf(o, "VALUE: 0x%lx\\n", self);
fprintf(o, "OBJ_FROZEN: %d\\n", OBJ_FROZEN(self));
fprintf(o, "STR_EMBED_P: %d\\n", str_embed);
fprintf(o, "STR_SHARED_P: %d\\n", str_shared);
fprintf(o, "RSTRING_PTR: %p\\n", rstring_ptr);
fprintf(o, "RSTRING_LEN: %ld\\n", rstring_len);
if (str_embed) {
#if USE_RVARGC
fprintf(o, "rstring->as.embed.len: %ld\\n", rstring->as.embed.len);
#endif
fprintf(o, "rstring->as.embed.ary: %p\\n", rstring->as.embed.ary);
fprintf(o, "rstring->as.embed.ary: \\"%s\\"\\n", rstring->as.embed.ary);
} else {
fprintf(o, "rstring->as.heap.len: %ld\\n", rstring->as.heap.len);
fprintf(o, "rstring->as.heap.ptr: %p\\n", rstring->as.heap.ptr);
if (str_shared) {
fprintf(o, "rstring->as.heap.aux.shared: 0x%lx\\n",
rstring->as.heap.aux.shared);
} else {
fprintf(o, "rstring->as.heap.aux.capa: %ld\\n",
rstring->as.heap.aux.capa);
}
}
fprintf(o, "\\n");
fflush(o);
return Qnil;
}
CODE
end
end
module Kernel
inline do |builder|
builder.add_compile_flags '-Wall'
# XXX: DANGER!!!
builder.c_raw <<~CODE
VALUE draw_object_by_value(int argc, VALUE *argv, VALUE _kernel) {
if (argc != 1)
return Qnil;
unsigned long value = (VALUE)NUM2ULONG(argv[0]);
return (VALUE)value;
}
CODE
end
end
% ruby -v
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-freebsd13.1]
% irb -I . -r inspect_rstring
irb(main):001:0> String.new("*" * 23).inspect_rstring
VALUE: 0x804f94cd0
OBJ_FROZEN: 0
STR_EMBED_P: 1
STR_SHARED_P: 1
RSTRING_PTR: 0x804f94ce0
RSTRING_LEN: 23
rstring->as.embed.ary: 0x804f94ce0
rstring->as.embed.ary: "***********************"
=> nil
irb(main):002:0> String.new("*" * 24).inspect_rstring
VALUE: 0x8062f0ab8
OBJ_FROZEN: 0
STR_EMBED_P: 0
STR_SHARED_P: 1
RSTRING_PTR: 0x8051b5760
RSTRING_LEN: 24
rstring->as.heap.len: 24
rstring->as.heap.ptr: 0x8051b5760
rstring->as.heap.aux.shared: 0x8062f0a90
=> nil
irb(main):003:0> String.new(capacity: 62).inspect_rstring
VALUE: 0x805ae96d0
OBJ_FROZEN: 0
STR_EMBED_P: 0
STR_SHARED_P: 0
RSTRING_PTR: 0x805292cc0
RSTRING_LEN: 0
rstring->as.heap.len: 0
rstring->as.heap.ptr: 0x805292cc0
rstring->as.heap.aux.capa: 63
=> nil
irb(main):004:0>
% ruby -v
ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-freebsd13.1]
% irb -I . -r inspect_rstring
irb(main):001:0> String.new("*" * 15).inspect_rstring
VALUE: 0x80c643700
OBJ_FROZEN: 0
STR_EMBED_P: 1
STR_SHARED_P: 0
RSTRING_PTR: 0x80c643718
RSTRING_LEN: 15
rstring->as.embed.len: 15
rstring->as.embed.ary: 0x80c643718
rstring->as.embed.ary: "***************"
=> nil
irb(main):002:0> String.new("*" * 16).inspect_rstring
VALUE: 0x80c604f00
OBJ_FROZEN: 0
STR_EMBED_P: 0
STR_SHARED_P: 1
RSTRING_PTR: 0x8084eb388
RSTRING_LEN: 16
rstring->as.heap.len: 16
rstring->as.heap.ptr: 0x8084eb388
rstring->as.heap.aux.shared: 0x8084eb370
=> nil
irb(main):003:0> String.new(capacity: 62).inspect_rstring
VALUE: 0x80c5c71c8
OBJ_FROZEN: 0
STR_EMBED_P: 0
STR_SHARED_P: 0
RSTRING_PTR: 0x807251b00
RSTRING_LEN: 0
rstring->as.heap.len: 0
rstring->as.heap.ptr: 0x807251b00
rstring->as.heap.aux.capa: 63
=> nil
irb(main):004:0>
elfham commented
# あれ?余談ですが、 3.1.3 では STR_EMBED_P
と STR_SHARED_P
の両方が立つことがあるんですね……。