Lock-free hash set for fstrings [Feature #21268]

This implements a hash set which is wait-free for lookup and lock-free
for insert (unless resizing) to use for fstring de-duplication.

As highlighted in https://bugs.ruby-lang.org/issues/19288, heavy use of
fstrings (frozen interned strings) can significantly reduce the
parallelism of Ractors.

I tried a few other approaches first: using an RWLock, striping a series
of RWlocks (partitioning the hash N-ways to reduce lock contention), and
putting a cache in front of it. All of these improved the situation, but
were unsatisfying as all still required locks for writes (and granular
locks are awkward, since we run the risk of needing to reach a vm
barrier) and this table is somewhat write-heavy.

My main reference for this was Cliff Click's talk on a lock free
hash-table for java https://www.youtube.com/watch?v=HJ-719EGIts. It
turns out this lock-free hash set is made easier to implement by a few
properties:

 * We only need a hash set rather than a hash table (we only need keys,
   not values), and so the full entry can be written as a single VALUE
 * As a set we only need lookup/insert/delete, no update
 * Delete is only run inside GC so does not need to be atomic (It could
   be made concurrent)
 * I use rb_vm_barrier for the (rare) table rebuilds (It could be made
   concurrent) We VM lock (but don't require other threads to stop) for
   table rebuilds, as those are rare
 * The conservative garbage collector makes deferred replication easy,
   using a T_DATA object

Another benefits of having a table specific to fstrings is that we
compare by value on lookup/insert, but by identity on delete, as we only
want to remove the exact string which is being freed. This is faster and
provides a second way to avoid the race condition in
https://bugs.ruby-lang.org/issues/21172.

This is a pretty standard open-addressing hash table with quadratic
probing. Similar to our existing st_table or id_table. Deletes (which
happen on GC) replace existing keys with a tombstone, which is the only
type of update which can occur. Tombstones are only cleared out on
resize.

Unlike st_table, the VALUEs are stored in the hash table itself
(st_table's bins) rather than as a compact index. This avoids an extra
pointer dereference and is possible because we don't need to preserve
insertion order. The table targets a load factor of 2 (it is enlarged
once it is half full).
This commit is contained in:
John Hawthorn 2025-03-06 16:16:35 -08:00
parent b28363a838
commit 57b6a7503f
Notes: git 2025-04-18 04:04:10 +00:00
8 changed files with 437 additions and 123 deletions

View File

@ -1580,6 +1580,21 @@ assert_equal "#{N}#{N}", %Q{
}.map{|r| r.take}.join
}
assert_equal "ok", %Q{
N = #{N}
a, b = 2.times.map{
Ractor.new{
N.times.map{|i| -(i.to_s)}
}
}.map{|r| r.take}
N.times do |i|
unless a[i].equal?(b[i])
raise [a[i], b[i]].inspect
end
end
:ok
}
# Generic ivtbl
n = N/2
assert_equal "#{n}#{n}", %Q{

1
eval.c
View File

@ -79,6 +79,7 @@ ruby_setup(void)
Init_BareVM();
rb_vm_encoded_insn_data_table_init();
Init_vm_objects();
Init_fstring_table();
EC_PUSH_TAG(GET_EC());
if ((state = EC_EXEC_TAG()) == TAG_NONE) {

15
gc.c
View File

@ -341,6 +341,7 @@ rb_gc_shutdown_call_finalizer_p(VALUE obj)
if (rb_obj_is_mutex(obj)) return false;
if (rb_obj_is_fiber(obj)) return false;
if (rb_obj_is_main_ractor(obj)) return false;
if (rb_obj_is_fstring_table(obj)) return false;
return true;
@ -3528,6 +3529,7 @@ vm_weak_table_frozen_strings_foreach(st_data_t key, st_data_t value, st_data_t d
return retval;
}
void rb_fstring_foreach_with_replace(st_foreach_check_callback_func *func, st_update_callback_func *replace, st_data_t arg);
void
rb_gc_vm_weak_table_foreach(vm_table_foreach_callback_func callback,
vm_table_update_callback_func update_callback,
@ -3590,14 +3592,11 @@ rb_gc_vm_weak_table_foreach(vm_table_foreach_callback_func callback,
break;
}
case RB_GC_VM_FROZEN_STRINGS_TABLE: {
if (vm->frozen_strings) {
st_foreach_with_replace(
vm->frozen_strings,
vm_weak_table_frozen_strings_foreach,
vm_weak_table_foreach_update_weak_key,
(st_data_t)&foreach_data
);
}
rb_fstring_foreach_with_replace(
vm_weak_table_frozen_strings_foreach,
vm_weak_table_foreach_update_weak_key,
(st_data_t)&foreach_data
);
break;
}
default:

View File

@ -84,6 +84,8 @@ RUBY_SYMBOL_EXPORT_END
VALUE rb_fstring_new(const char *ptr, long len);
void rb_gc_free_fstring(VALUE obj);
bool rb_obj_is_fstring_table(VALUE obj);
void Init_fstring_table();
VALUE rb_obj_as_string_result(VALUE str, VALUE obj);
VALUE rb_str_opt_plus(VALUE x, VALUE y);
VALUE rb_str_concat_literals(size_t num, const VALUE *strary);

View File

@ -56,7 +56,6 @@ void rb_vm_check_redefinition_by_prepend(VALUE klass);
int rb_vm_check_optimizable_mid(VALUE mid);
VALUE rb_yield_refine_block(VALUE refinement, VALUE refinements);
VALUE ruby_vm_special_exception_copy(VALUE);
PUREFUNC(st_table *rb_vm_fstring_table(void));
void rb_lastline_set_up(VALUE val, unsigned int up);

512
string.c
View File

@ -383,11 +383,6 @@ fstring_hash(VALUE str)
#define fstring_hash rb_str_hash
#endif
const struct st_hash_type rb_fstring_hash_type = {
fstring_cmp,
fstring_hash,
};
#define BARE_STRING_P(str) (!FL_ANY_RAW(str, FL_EXIVAR) && RBASIC_CLASS(str) == rb_cString)
static inline st_index_t
@ -421,90 +416,72 @@ str_store_precomputed_hash(VALUE str, st_index_t hash)
}
struct fstr_update_arg {
VALUE fstr;
bool copy;
bool force_precompute_hash;
};
static int
fstr_update_callback(st_data_t *key, st_data_t *value, st_data_t data, int existing)
{
struct fstr_update_arg *arg = (struct fstr_update_arg *)data;
VALUE str = (VALUE)*key;
static VALUE build_fstring(VALUE str, struct fstr_update_arg *arg) {
// Unless the string is empty or binary, its coderange has been precomputed.
int coderange = ENC_CODERANGE(str);
if (existing) {
/* because of lazy sweep, str may be unmarked already and swept
* at next time */
if (FL_TEST_RAW(str, STR_FAKESTR)) {
if (arg->copy) {
VALUE new_str;
long len = RSTRING_LEN(str);
long capa = len + sizeof(st_index_t);
int term_len = TERM_LEN(str);
if (rb_objspace_garbage_object_p(str)) {
arg->fstr = Qundef;
// When RSTRING_FSTR strings are swept, they call `st_delete`.
// To avoid a race condition if an equivalent string was inserted
// we must remove the flag immediately.
FL_UNSET_RAW(str, RSTRING_FSTR);
return ST_DELETE;
}
arg->fstr = str;
return ST_STOP;
}
else {
// Unless the string is empty or binary, its coderange has been precomputed.
int coderange = ENC_CODERANGE(str);
if (FL_TEST_RAW(str, STR_FAKESTR)) {
if (arg->copy) {
VALUE new_str;
long len = RSTRING_LEN(str);
long capa = len + sizeof(st_index_t);
int term_len = TERM_LEN(str);
if (arg->force_precompute_hash && STR_EMBEDDABLE_P(capa, term_len)) {
new_str = str_alloc_embed(rb_cString, capa + term_len);
memcpy(RSTRING_PTR(new_str), RSTRING_PTR(str), len);
STR_SET_LEN(new_str, RSTRING_LEN(str));
TERM_FILL(RSTRING_END(new_str), TERM_LEN(str));
rb_enc_copy(new_str, str);
str_store_precomputed_hash(new_str, str_do_hash(str));
}
else {
new_str = str_new(rb_cString, RSTRING(str)->as.heap.ptr, RSTRING(str)->len);
rb_enc_copy(new_str, str);
#ifdef PRECOMPUTED_FAKESTR_HASH
if (rb_str_capacity(new_str) >= RSTRING_LEN(str) + term_len + sizeof(st_index_t)) {
str_store_precomputed_hash(new_str, (st_index_t)RSTRING(str)->as.heap.aux.capa);
}
#endif
}
str = new_str;
if (arg->force_precompute_hash && STR_EMBEDDABLE_P(capa, term_len)) {
new_str = str_alloc_embed(rb_cString, capa + term_len);
memcpy(RSTRING_PTR(new_str), RSTRING_PTR(str), len);
STR_SET_LEN(new_str, RSTRING_LEN(str));
TERM_FILL(RSTRING_END(new_str), TERM_LEN(str));
rb_enc_copy(new_str, str);
str_store_precomputed_hash(new_str, str_do_hash(str));
}
else {
str = str_new_static(rb_cString, RSTRING(str)->as.heap.ptr,
RSTRING(str)->len,
ENCODING_GET(str));
new_str = str_new(rb_cString, RSTRING(str)->as.heap.ptr, RSTRING(str)->len);
rb_enc_copy(new_str, str);
#ifdef PRECOMPUTED_FAKESTR_HASH
if (rb_str_capacity(new_str) >= RSTRING_LEN(str) + term_len + sizeof(st_index_t)) {
str_store_precomputed_hash(new_str, (st_index_t)RSTRING(str)->as.heap.aux.capa);
}
#endif
}
OBJ_FREEZE(str);
str = new_str;
}
else {
if (!OBJ_FROZEN(str) || CHILLED_STRING_P(str)) {
str = str_new_frozen(rb_cString, str);
}
if (STR_SHARED_P(str)) { /* str should not be shared */
/* shared substring */
str_make_independent(str);
RUBY_ASSERT(OBJ_FROZEN(str));
}
if (!BARE_STRING_P(str)) {
str = str_new_frozen(rb_cString, str);
}
str = str_new_static(rb_cString, RSTRING(str)->as.heap.ptr,
RSTRING(str)->len,
ENCODING_GET(str));
}
ENC_CODERANGE_SET(str, coderange);
RBASIC(str)->flags |= RSTRING_FSTR;
*key = *value = arg->fstr = str;
return ST_CONTINUE;
OBJ_FREEZE(str);
}
else {
if (!OBJ_FROZEN(str) || CHILLED_STRING_P(str)) {
str = str_new_frozen(rb_cString, str);
}
if (STR_SHARED_P(str)) { /* str should not be shared */
/* shared substring */
str_make_independent(str);
RUBY_ASSERT(OBJ_FROZEN(str));
}
if (!BARE_STRING_P(str)) {
str = str_new_frozen(rb_cString, str);
}
}
ENC_CODERANGE_SET(str, coderange);
RBASIC(str)->flags |= RSTRING_FSTR;
RUBY_ASSERT(RB_TYPE_P(str, T_STRING));
RUBY_ASSERT(OBJ_FROZEN(str));
RUBY_ASSERT(!FL_TEST_RAW(str, STR_FAKESTR));
RUBY_ASSERT(!FL_TEST_RAW(str, FL_EXIVAR));
RUBY_ASSERT(RBASIC_CLASS(str) == rb_cString);
RUBY_ASSERT(!rb_objspace_garbage_object_p(str));
return str;
}
VALUE
@ -544,6 +521,298 @@ rb_fstring(VALUE str)
return fstr;
}
#define FSTRING_TABLE_EMPTY Qfalse
#define FSTRING_TABLE_TOMBSTONE Qtrue
#define FSTRING_TABLE_MOVED Qundef
struct fstring_table_entry {
VALUE str;
VALUE hash;
};
struct fstring_table_struct {
struct fstring_table_entry *entries;
int capacity;
int deleted_entries;
rb_atomic_t count; // TODO: pad to own cache line?
};
static void fstring_table_free(void *ptr) {
struct fstring_table_struct *table = ptr;
xfree(table->entries);
}
// We declare a type for the table so that we can lean on Ruby's GC for deferred reclamation
static const rb_data_type_t fstring_table_type = {
.wrap_struct_name = "fstring_table",
.function = {
.dmark = NULL,
.dfree = fstring_table_free,
.dsize = NULL,
},
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE
};
static VALUE fstring_table_obj;
static VALUE
new_fstring_table(int capacity) {
VALUE obj;
struct fstring_table_struct *table;
obj = TypedData_Make_Struct(0, struct fstring_table_struct, &fstring_table_type, table);
table->capacity = capacity;
table->count = 0;
table->entries = ZALLOC_N(struct fstring_table_entry, capacity);
return obj;
}
void Init_fstring_table(void) {
fstring_table_obj = new_fstring_table(8192);
rb_gc_register_address(&fstring_table_obj);
}
#if 0
// Linear probe
struct fstring_table_probe {
int idx;
int mask;
};
static int fstring_table_probe_start(struct fstring_table_probe *probe, struct fstring_table_struct *table, VALUE hash_code) {
RUBY_ASSERT((table->capacity & (table->capacity - 1)) == 0);
probe->mask = table->capacity - 1;
probe->idx = hash_code & probe->mask;
return probe->idx;
}
static int fstring_table_probe_next(struct fstring_table_probe *probe) {
probe->idx = (probe->idx + 1) & probe->mask;
return probe->idx;
}
#else
// Struct containing probe information. Intended that the compiler should always inline this
// Quadratic probing
struct fstring_table_probe {
int idx;
int d;
int mask;
};
static int fstring_table_probe_start(struct fstring_table_probe *probe, struct fstring_table_struct *table, VALUE hash_code) {
RUBY_ASSERT((table->capacity & (table->capacity - 1)) == 0);
probe->d = 0;
probe->mask = table->capacity - 1;
probe->idx = hash_code & probe->mask;
return probe->idx;
}
static int fstring_table_probe_next(struct fstring_table_probe *probe) {
probe->d++;
probe->idx = (probe->idx + probe->d) & probe->mask;
return probe->idx;
}
#endif
#define RUBY_ATOMIC_VALUE_LOAD(x) (VALUE)(RUBY_ATOMIC_PTR_LOAD(x))
static void fstring_insert_on_resize(struct fstring_table_struct *table, VALUE hash_code, VALUE value) {
struct fstring_table_probe probe;
int idx = fstring_table_probe_start(&probe, table, hash_code);
for (;;) {
struct fstring_table_entry *entry = &table->entries[idx];
VALUE candidate = entry->str;
RUBY_ASSERT(candidate != FSTRING_TABLE_TOMBSTONE);
RUBY_ASSERT(candidate != FSTRING_TABLE_MOVED);
if (candidate == FSTRING_TABLE_EMPTY) {
table->count++;
RUBY_ASSERT(table->count < table->capacity / 2);
RUBY_ASSERT(entry->hash == 0);
entry->str = value;
entry->hash = hash_code;
return;
}
idx = fstring_table_probe_next(&probe);
}
}
// Rebuilds the table
static void fstring_try_resize(VALUE old_table_obj) {
RB_VM_LOCK_ENTER();
// Check if another thread has already resized
if (RUBY_ATOMIC_VALUE_LOAD(fstring_table_obj) != old_table_obj) {
goto end;
}
struct fstring_table_struct *old_table = RTYPEDDATA_GET_DATA(old_table_obj);
// This may overcount by up to the number of threads concurrently attempting to insert
// GC may also happen between now and the table being rebuilt
int expected_count = RUBY_ATOMIC_LOAD(old_table->count) - old_table->deleted_entries;
struct fstring_table_entry *old_entries = old_table->entries;
int old_capacity = old_table->capacity;
int new_capacity = old_capacity * 2;
if (new_capacity > expected_count * 8) {
new_capacity = old_capacity / 2;
}
else if (new_capacity > expected_count * 4) {
new_capacity = old_capacity;
}
// May cause GC and therefore deletes, so must hapen first
VALUE new_table_obj = new_fstring_table(new_capacity);
struct fstring_table_struct *new_table = RTYPEDDATA_GET_DATA(new_table_obj);
for (int i = 0; i < old_capacity; i++) {
struct fstring_table_entry *entry = &old_entries[i];
VALUE val = RUBY_ATOMIC_VALUE_EXCHANGE(entry->str, FSTRING_TABLE_MOVED);
RUBY_ASSERT(val != FSTRING_TABLE_MOVED);
if (val == FSTRING_TABLE_EMPTY) continue;
if (val == FSTRING_TABLE_TOMBSTONE) continue;
if (rb_objspace_garbage_object_p(val)) continue;
VALUE hash_code = RUBY_ATOMIC_VALUE_LOAD(entry->hash);
if (hash_code == 0) {
// Either in-progress insert or extremely unlikely 0 hash
// Re-calculate the hash ourselves
hash_code = fstring_hash(val);
}
RUBY_ASSERT(hash_code == fstring_hash(val));
fstring_insert_on_resize(new_table, hash_code, val);
}
#if 0
fprintf(stderr, "resized: %p(%i) -> %p(%i) (count: %i->%i)\n", old_table, old_table->capacity, new_table, new_table->capacity, old_table->count, new_table->count);
#endif
RUBY_ATOMIC_VALUE_SET(fstring_table_obj, new_table_obj);
end:
RB_GC_GUARD(old_table_obj);
RB_VM_LOCK_LEAVE();
}
static VALUE fstring_find_or_insert(VALUE hash_code, VALUE value, struct fstr_update_arg *arg) {
struct fstring_table_probe probe;
bool inserting = false;
int idx;
VALUE table_obj;
struct fstring_table_struct *table;
retry:
table_obj = RUBY_ATOMIC_VALUE_LOAD(fstring_table_obj);
RUBY_ASSERT(table_obj);
table = RTYPEDDATA_GET_DATA(table_obj);
idx = fstring_table_probe_start(&probe, table, hash_code);
for (;;) {
struct fstring_table_entry *entry = &table->entries[idx];
VALUE candidate = RUBY_ATOMIC_VALUE_LOAD(entry->str);
if (candidate == FSTRING_TABLE_EMPTY) {
// Not in table
if (!inserting) {
// Prepare a string suitable for inserting into the table
value = build_fstring(value, arg);
RUBY_ASSERT(hash_code == fstring_hash(value));
inserting = true;
}
int prev_count = RUBY_ATOMIC_FETCH_ADD(table->count, 1);
if (UNLIKELY(prev_count > table->capacity / 2)) {
fstring_try_resize(table_obj);
goto retry;
}
VALUE found = RUBY_ATOMIC_VALUE_CAS(entry->str, FSTRING_TABLE_EMPTY, value);
if (found == FSTRING_TABLE_EMPTY) {
// Success! Our value was inserted
// Also set the hash code
RUBY_ATOMIC_VALUE_SET(entry->hash, hash_code);
RB_GC_GUARD(table_obj);
return value;
} else {
// Nothing was inserted
RUBY_ATOMIC_DEC(table->count); // we didn't end up inserting
// Another thread won the race, try again at the same location
continue;
}
} else if (candidate == FSTRING_TABLE_TOMBSTONE) {
// Deleted entry, continue searching
} else if (candidate == FSTRING_TABLE_MOVED) {
// Wait
RB_VM_LOCK_ENTER();
RB_VM_LOCK_LEAVE();
goto retry;
} else {
VALUE candidate_hash = RUBY_ATOMIC_VALUE_LOAD(entry->hash);
if ((candidate_hash == hash_code || candidate_hash == 0) && !fstring_cmp(candidate, value)) {
// We've found a match
if (UNLIKELY(rb_objspace_garbage_object_p(candidate))) {
// This is a weakref table, so after marking but before sweeping is complete we may find a matching garbage object.
// Skip it and mark it as a tombstone to help other threads out
RUBY_ATOMIC_VALUE_CAS(entry->str, candidate, FSTRING_TABLE_TOMBSTONE);
// Fall through and continue our search
} else {
RB_GC_GUARD(table_obj);
return candidate;
}
}
}
idx = fstring_table_probe_next(&probe);
}
}
// Removes an fstring from the table. Compares by identity
static void fstring_delete(VALUE hash_code, VALUE value) {
// Delete is never called concurrently, so atomic operations are unnecessary
VALUE table_obj = RUBY_ATOMIC_VALUE_LOAD(fstring_table_obj);
RUBY_ASSERT_ALWAYS(table_obj);
struct fstring_table_struct *table = RTYPEDDATA_GET_DATA(table_obj);
struct fstring_table_probe probe;
int idx = fstring_table_probe_start(&probe, table, hash_code);
for (;;) {
struct fstring_table_entry *entry = &table->entries[idx];
VALUE candidate = entry->str;
// Allocations should only occur at the beginning of the resize
RUBY_ASSERT(candidate != FSTRING_TABLE_MOVED);
if (candidate == FSTRING_TABLE_EMPTY) {
// We didn't find our string to delete
return;
} else if (candidate == value) {
// We found our string, replace it with a tombstone and increment the count
entry->str = FSTRING_TABLE_TOMBSTONE;
table->deleted_entries++;
return;
}
idx = fstring_table_probe_next(&probe);
}
}
static VALUE
register_fstring(VALUE str, bool copy, bool force_precompute_hash)
{
@ -560,29 +829,71 @@ register_fstring(VALUE str, bool copy, bool force_precompute_hash)
}
#endif
RB_VM_LOCK_ENTER();
{
st_table *frozen_strings = rb_vm_fstring_table();
do {
args.fstr = str;
st_update(frozen_strings, (st_data_t)str, fstr_update_callback, (st_data_t)&args);
} while (UNDEF_P(args.fstr));
VALUE hash_code = fstring_hash(str);
VALUE result = fstring_find_or_insert(hash_code, str, &args);
RUBY_ASSERT(!rb_objspace_garbage_object_p(result));
RUBY_ASSERT(RB_TYPE_P(result, T_STRING));
RUBY_ASSERT(OBJ_FROZEN(result));
RUBY_ASSERT(!FL_TEST_RAW(result, STR_FAKESTR));
RUBY_ASSERT(!FL_TEST_RAW(result, FL_EXIVAR));
RUBY_ASSERT(RBASIC_CLASS(result) == rb_cString);
return result;
}
void rb_fstring_foreach_with_replace(st_foreach_check_callback_func *func, st_update_callback_func *replace, st_data_t arg) {
// Assume locking and barrier (which there is no assert for)
ASSERT_vm_locking();
VALUE table_obj = RUBY_ATOMIC_VALUE_LOAD(fstring_table_obj);
if (!table_obj) {
// Table not yet initialized. Nothing to iterate over
return;
}
RB_VM_LOCK_LEAVE();
struct fstring_table_struct *table = RTYPEDDATA_GET_DATA(table_obj);
RUBY_ASSERT(OBJ_FROZEN(args.fstr));
RUBY_ASSERT(!FL_TEST_RAW(args.fstr, STR_FAKESTR));
RUBY_ASSERT(!FL_TEST_RAW(args.fstr, FL_EXIVAR));
RUBY_ASSERT(RBASIC_CLASS(args.fstr) == rb_cString);
for (int i = 0; i < table->capacity; i++) {
VALUE key = table->entries[i].str;
if(key == FSTRING_TABLE_EMPTY) continue;
if(key == FSTRING_TABLE_TOMBSTONE) continue;
return args.fstr;
enum st_retval retval;
retval = (*func)(key, key, arg, 0);
if (retval == ST_REPLACE && replace) {
st_data_t value = key;
retval = (*replace)(&key, &value, arg, TRUE);
table->entries[i].str = key;
}
switch (retval) {
case ST_REPLACE:
case ST_CONTINUE:
break;
case ST_CHECK:
rb_bug("unsupported");
case ST_STOP:
return;
case ST_DELETE:
table->entries[i].str = FSTRING_TABLE_TOMBSTONE;
break;
}
}
}
bool rb_obj_is_fstring_table(VALUE obj) {
ASSERT_vm_locking();
return obj == fstring_table_obj;
}
void rb_gc_free_fstring(VALUE obj) {
// Assume locking and barrier (which there is no assert for)
ASSERT_vm_locking();
st_data_t fstr = (st_data_t)obj;
st_delete(rb_vm_fstring_table(), &fstr, NULL);
VALUE str_hash = fstring_hash(obj);
fstring_delete(str_hash, obj);
RB_DEBUG_COUNTER_INC(obj_str_fstr);
FL_UNSET(obj, RSTRING_FSTR);
@ -641,18 +952,15 @@ rb_fstring_cstr(const char *ptr)
return rb_fstring_new(ptr, strlen(ptr));
}
static int
fstring_set_class_i(st_data_t key, st_data_t val, st_data_t arg)
{
RBASIC_SET_CLASS((VALUE)key, (VALUE)arg);
return ST_CONTINUE;
}
static int
fstring_cmp(VALUE a, VALUE b)
{
long alen, blen;
const char *aptr, *bptr;
RUBY_ASSERT(RB_TYPE_P(a, T_STRING));
RUBY_ASSERT(RB_TYPE_P(b, T_STRING));
RSTRING_GETMEM(a, aptr, alen);
RSTRING_GETMEM(b, bptr, blen);
return (alen != blen ||
@ -12636,8 +12944,12 @@ void
Init_String(void)
{
rb_cString = rb_define_class("String", rb_cObject);
RUBY_ASSERT(rb_vm_fstring_table());
st_foreach(rb_vm_fstring_table(), fstring_set_class_i, rb_cString);
struct fstring_table_struct *fstring_table = RTYPEDDATA_GET_DATA(fstring_table_obj);
for (int i = 0; i < fstring_table->capacity; i++) {
VALUE str = fstring_table->entries[i].str;
if (!str) continue;
RBASIC_SET_CLASS(str, rb_cString);
}
rb_include_module(rb_cString, rb_mComparable);
rb_define_alloc_func(rb_cString, empty_str_alloc);
rb_define_singleton_method(rb_cString, "new", rb_str_s_new, -1);

12
vm.c
View File

@ -3134,10 +3134,6 @@ ruby_vm_destruct(rb_vm_t *vm)
st_free_table(vm->ci_table);
vm->ci_table = NULL;
}
if (vm->frozen_strings) {
st_free_table(vm->frozen_strings);
vm->frozen_strings = 0;
}
RB_ALTSTACK_FREE(vm->main_altstack);
struct global_object_list *next;
@ -3236,7 +3232,6 @@ vm_memsize(const void *ptr)
rb_vm_memsize_workqueue(&vm->workqueue) +
vm_memsize_at_exit_list(vm->at_exit) +
rb_st_memsize(vm->ci_table) +
rb_st_memsize(vm->frozen_strings) +
vm_memsize_builtin_function_table(vm->builtin_function_table) +
rb_id_table_memsize(vm->negative_cme_table) +
rb_st_memsize(vm->overloaded_cme_table) +
@ -4441,7 +4436,6 @@ Init_vm_objects(void)
vm->mark_object_ary = pin_array_list_new(Qnil);
vm->loading_table = st_init_strtable();
vm->ci_table = st_init_table(&vm_ci_hashtype);
vm->frozen_strings = st_init_table_with_size(&rb_fstring_hash_type, 10000);
}
// Stub for builtin function when not building YJIT units
@ -4503,12 +4497,6 @@ VALUE rb_insn_operand_intern(const rb_iseq_t *iseq,
VALUE insn, int op_no, VALUE op,
int len, size_t pos, VALUE *pnop, VALUE child);
st_table *
rb_vm_fstring_table(void)
{
return GET_VM()->frozen_strings;
}
#if VM_COLLECT_USAGE_DETAILS
#define HASH_ASET(h, k, v) rb_hash_aset((h), (st_data_t)(k), (st_data_t)(v))

View File

@ -778,8 +778,6 @@ typedef struct rb_vm_struct {
rb_at_exit_list *at_exit;
st_table *frozen_strings;
const struct rb_builtin_function *builtin_function_table;
st_table *ci_table;