Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions mrbgems/mruby-string-ext/test/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,77 @@ def assert_upto(exp, receiver, *args)
end
end

assert('String#concat on a shared buffer') do
# An append to a string that shares its buffer writes into the spare
# capacity above what every other sharer can see, and each sharer has to
# keep the bytes it owns. Every string below is grown by appending before
# it is shared, since a string that was never appended to has no spare
# capacity and every append to it copies the buffer instead.

# The parent appends in place; an interior slice must not see it.
a = "a" * 100
a << "z" * 100
mid = a[0, 100]
a << "q"
assert_equal "a" * 100 + "z" * 100 + "q", a
assert_equal "a" * 100, mid
mid << "r"
assert_equal "a" * 100 + "r", mid
assert_equal "a" * 100 + "z" * 100 + "q", a

# Two strings that end at the same offset: the first append claims the
# bytes, and the other one has to take a copy of the buffer.
b = "b" * 100
b << "y" * 100
c = b.dup
b << "1"
c << "2"
assert_equal "b" * 100 + "y" * 100 + "1", b
assert_equal "b" * 100 + "y" * 100 + "2", c

# The same in the other order.
d = "d" * 100
d << "w" * 100
e = d.dup
e << "1"
d << "2"
assert_equal "d" * 100 + "w" * 100 + "1", e
assert_equal "d" * 100 + "w" * 100 + "2", d

# A tail slice ends where its parent does, so the same rule applies.
f = "f" * 100
f << "v" * 100
g = f[170, 30]
g << "1"
f << "2"
assert_equal "v" * 30 + "1", g
assert_equal "f" * 100 + "v" * 100 + "2", f

# Appending and slicing in a loop, the shape the copy made quadratic.
h = ""
50.times { h << "0123456789012345678901234567890123456789"; h[0, 30] }
assert_equal 2000, h.length
assert_equal "0123456789012345678901234567890123456789", h[-40, 40]

# Appending a slice of a buffer to the string it was taken from.
i = "i" * 100
i << "j" * 100
k = i[0, 40]
i << k
assert_equal "i" * 100 + "j" * 100 + "i" * 40, i
assert_equal "i" * 40, k

# A frozen sharer still raises, and does not stop the others.
l = "l" * 100
l << "m" * 100
n = l.dup
n.freeze
assert_raise(FrozenError) { n << "x" }
l << "y"
assert_equal "l" * 100 + "m" * 100 + "y", l
assert_equal "l" * 100 + "m" * 100, n
end

assert('String#casecmp') do
assert_equal 1, "abcdef".casecmp("abcde")
assert_equal 0, "aBcDeF".casecmp("abcdef")
Expand Down
62 changes: 51 additions & 11 deletions src/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
typedef struct mrb_shared_string {
int refcnt;
mrb_int capa;
/* Offset past the last byte any sharer can see. Bytes at or above it are
dead to every sharer, so a writer may use them in place (str_modify_cat).
Only grows, as sharers are added. */
mrb_int reserved;
char *ptr;
} mrb_shared_string;

Expand Down Expand Up @@ -109,13 +113,16 @@ static struct RString*
str_init_shared(mrb_state *mrb, const struct RString *orig, struct RString *s, mrb_shared_string *shared)
{
if (shared) {
mrb_int end = (mrb_int)(orig->as.heap.ptr - shared->ptr) + orig->as.heap.len;
if (shared->reserved < end) shared->reserved = end;
shared->refcnt++;
}
else {
shared = (mrb_shared_string*)mrb_malloc(mrb, sizeof(mrb_shared_string));
shared->refcnt = 1;
shared->ptr = orig->as.heap.ptr;
shared->capa = orig->as.heap.aux.capa;
shared->reserved = orig->as.heap.len;
}
s->as.heap.ptr = orig->as.heap.ptr;
s->as.heap.len = orig->as.heap.len;
Expand Down Expand Up @@ -766,10 +773,8 @@ str_share(mrb_state *mrb, struct RString *orig, struct RString *s)
str_init_fshared(orig, s, orig->as.heap.aux.fshared);
}
else {
if (orig->as.heap.aux.capa > orig->as.heap.len) {
orig->as.heap.ptr = (char*)mrb_realloc(mrb, orig->as.heap.ptr, len+1);
orig->as.heap.aux.capa = (mrb_ssize)len;
}
/* Spare capacity is kept, not trimmed: it lies above `reserved`, so
`orig` can still append into it without copying the buffer. */
str_init_shared(mrb, orig, s, NULL);
str_init_shared(mrb, orig, orig, s->as.heap.aux.shared);
}
Expand Down Expand Up @@ -3118,6 +3123,37 @@ mrb_str_dump(mrb_state *mrb, mrb_value str)
return str_escape(mrb, str, FALSE);
}

/* mrb_str_modify() for appending `addlen` bytes at the end of `s`.
An append only touches [len, len+addlen), which no other sharer of the
buffer can see, so the buffer copy that mrb_str_modify() would do can be
skipped as long as the write stays inside the shared allocation. Growing
past it still has to detach, but capacity grows geometrically, so the
copies are amortized instead of one per append.
`addlen` must not be negative: it would pass the capacity guard below and
then lower `reserved`, handing bytes another sharer still reads to the
appender. mrb_str_cat() rejects a length that does not fit beforehand.
Returns the usable capacity of `s`. */
static mrb_int
str_modify_cat(mrb_state *mrb, struct RString *s, mrb_int addlen)
{
mrb_assert(addlen >= 0);
if (RSTR_SHARED_P(s)) {
mrb_check_frozen(mrb, s);
mrb_shared_string *shared = s->as.heap.aux.shared;
mrb_int off = (mrb_int)(s->as.heap.ptr - shared->ptr);
mrb_int capa = shared->capa - off;
if (off + s->as.heap.len >= shared->reserved && addlen < capa - s->as.heap.len) {
/* The appended bytes belong to `s` from now on, so no other sharer may
write over them. */
shared->reserved = off + s->as.heap.len + addlen;
RSTR_UNSET_SINGLE_BYTE_FLAG(s);
return capa;
}
}
mrb_str_modify(mrb, s);
return RSTR_CAPA(s);
}

/*
* @param mrb The mruby state.
* @param str The mruby string to append to (modified in place).
Expand All @@ -3136,17 +3172,21 @@ mrb_str_cat(mrb_state *mrb, mrb_value str, const char *ptr, size_t len)
ptrdiff_t off = -1;

if (len == 0) return str;
mrb_str_modify(mrb, s);
if (ptr >= RSTR_PTR(s) && ptr <= RSTR_PTR(s) + (size_t)RSTR_LEN(s)) {
off = ptr - RSTR_PTR(s);
}

mrb_int capa = RSTR_CAPA(s);
/* `len` has to be known to fit in an `mrb_int` before it is used as one:
the conversion is otherwise free to make it negative, and the overflow
check takes `mrb_int` parameters, so it would not see it. Checking ahead
of the modification also leaves the string untouched when it raises. */
mrb_int total;
if (mrb_int_add_overflow(RSTR_LEN(s), len, &total)) {
if (len > (size_t)MRB_INT_MAX ||
mrb_int_add_overflow(RSTR_LEN(s), (mrb_int)len, &total)) {
size_error:
mrb_raise(mrb, E_ARGUMENT_ERROR, "string size too big");
}
mrb_int capa = str_modify_cat(mrb, s, (mrb_int)len);
if (ptr >= RSTR_PTR(s) && ptr <= RSTR_PTR(s) + (size_t)RSTR_LEN(s)) {
off = ptr - RSTR_PTR(s);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (capa <= total) {
if (capa == 0) capa = 1;
while (capa <= total) {
Expand Down
Loading