Ruby 4.1.0dev (2026-09-07 revision b57404b461ba8bf34e802d86b0db78388216e182)
concurrent_set.c (b57404b461ba8bf34e802d86b0db78388216e182)
1#include "internal.h"
2#include "internal/gc.h"
3#include "internal/concurrent_set.h"
4#include "ruby/atomic.h"
5#include "ruby/ractor.h"
6#include "vm_sync.h"
7
8#define CONCURRENT_SET_CONTINUATION_BIT ((VALUE)1 << (sizeof(VALUE) * CHAR_BIT - 1))
9#define CONCURRENT_SET_HASH_MASK (~CONCURRENT_SET_CONTINUATION_BIT)
10
11enum concurrent_set_special_values {
12 CONCURRENT_SET_EMPTY,
13 CONCURRENT_SET_DELETED,
14 CONCURRENT_SET_MOVED,
15 CONCURRENT_SET_SPECIAL_VALUE_COUNT
16};
17
19 VALUE hash;
20 VALUE key;
21};
22
24 rb_atomic_t size;
25 unsigned int capacity;
26 unsigned int deleted_entries;
27 const struct rb_concurrent_set_funcs *funcs;
28 struct concurrent_set_entry *entries;
29};
30
31static void
32concurrent_set_mark_continuation(struct concurrent_set_entry *entry, VALUE curr_hash_and_flags)
33{
34 if (curr_hash_and_flags & CONCURRENT_SET_CONTINUATION_BIT) return;
35
36 RUBY_ASSERT((curr_hash_and_flags & CONCURRENT_SET_HASH_MASK) != 0);
37
38 VALUE new_hash = curr_hash_and_flags | CONCURRENT_SET_CONTINUATION_BIT;
39 VALUE prev_hash = rbimpl_atomic_value_cas(&entry->hash, curr_hash_and_flags, new_hash, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED);
40
41 // At the moment we only expect to be racing concurrently against another
42 // thread also setting the continuation bit.
43 // In the future if deletion is concurrent this will need adjusting
44 RUBY_ASSERT(prev_hash == curr_hash_and_flags || prev_hash == new_hash);
45 (void)prev_hash;
46}
47
48static VALUE
49concurrent_set_hash(const struct concurrent_set *set, VALUE key)
50{
51 VALUE hash = set->funcs->hash(key);
52 hash &= CONCURRENT_SET_HASH_MASK;
53 if (hash == 0) {
54 hash ^= CONCURRENT_SET_HASH_MASK;
55 }
56 RUBY_ASSERT(hash != 0);
57 RUBY_ASSERT(!(hash & CONCURRENT_SET_CONTINUATION_BIT));
58 return hash;
59}
60
61static void
62concurrent_set_free(void *ptr)
63{
64 struct concurrent_set *set = ptr;
65 SIZED_FREE_N(set->entries, set->capacity);
66}
67
68static size_t
69concurrent_set_size(const void *ptr)
70{
71 const struct concurrent_set *set = ptr;
72 return sizeof(struct concurrent_set) +
73 (set->capacity * sizeof(struct concurrent_set_entry));
74}
75
76/* Hack: Though it would be trivial, we're intentionally avoiding WB-protecting
77 * this object. This prevents the object from aging and ensures it can always be
78 * collected in a minor GC.
79 * Longer term this deserves a better way to reclaim memory promptly.
80 */
81static void
82concurrent_set_mark(void *ptr)
83{
84 (void)ptr;
85}
86
87static const rb_data_type_t concurrent_set_type = {
88 .wrap_struct_name = "VM/concurrent_set",
89 .function = {
90 .dmark = concurrent_set_mark,
91 .dfree = concurrent_set_free,
92 .dsize = concurrent_set_size,
93 },
94 /* Hack: NOT WB_PROTECTED on purpose (see above) */
95 .flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_EMBEDDABLE
96};
97
99rb_concurrent_set_new(const struct rb_concurrent_set_funcs *funcs, int capacity)
100{
101 struct concurrent_set *set;
102 VALUE obj = TypedData_Make_Struct(0, struct concurrent_set, &concurrent_set_type, set);
103 set->funcs = funcs;
104 set->entries = ZALLOC_N(struct concurrent_set_entry, capacity);
105 set->capacity = capacity;
106 /* The set is reachable from every Ractor (e.g. via C globals such as the
107 * frozen-string and symbol tables), so mark it shareable. */
108 RB_OBJ_SET_SHAREABLE(obj);
109 return obj;
110}
111
113rb_concurrent_set_size(VALUE set_obj)
114{
115 struct concurrent_set *set = RTYPEDDATA_GET_DATA(set_obj);
116
117 return RUBY_ATOMIC_LOAD(set->size);
118}
119
121 int idx;
122 int d;
123 int mask;
124};
125
126static int
127concurrent_set_probe_start(struct concurrent_set_probe *probe, struct concurrent_set *set, VALUE hash)
128{
129 RUBY_ASSERT((set->capacity & (set->capacity - 1)) == 0);
130 probe->d = 0;
131 probe->mask = set->capacity - 1;
132 probe->idx = hash & probe->mask;
133 return probe->idx;
134}
135
136static int
137concurrent_set_probe_next(struct concurrent_set_probe *probe)
138{
139 probe->d++;
140 probe->idx = (probe->idx + probe->d) & probe->mask;
141 return probe->idx;
142}
143
144static void
145concurrent_set_try_resize_without_locking(VALUE old_set_obj, VALUE *set_obj_ptr)
146{
147 // Check if another thread has already resized.
148 if (rbimpl_atomic_value_load(set_obj_ptr, RBIMPL_ATOMIC_ACQUIRE) != old_set_obj) {
149 return;
150 }
151
152 struct concurrent_set *old_set = RTYPEDDATA_GET_DATA(old_set_obj);
153
154 // This may overcount by up to the number of threads concurrently attempting to insert
155 // GC may also happen between now and the set being rebuilt
156 int expected_size = rbimpl_atomic_load(&old_set->size, RBIMPL_ATOMIC_RELAXED) - old_set->deleted_entries;
157
158 // NOTE: new capacity must make sense with load factor, don't change one without checking the other.
159 struct concurrent_set_entry *old_entries = old_set->entries;
160 int old_capacity = old_set->capacity;
161 int new_capacity = old_capacity * 2;
162 if (new_capacity > expected_size * 8) {
163 new_capacity = old_capacity / 2;
164 }
165 else if (new_capacity > expected_size * 4) {
166 new_capacity = old_capacity;
167 }
168
169 // May cause GC and therefore deletes, so must happen first.
170 VALUE new_set_obj = rb_concurrent_set_new(old_set->funcs, new_capacity);
171 struct concurrent_set *new_set = RTYPEDDATA_GET_DATA(new_set_obj);
172
173 for (int i = 0; i < old_capacity; i++) {
174 struct concurrent_set_entry *old_entry = &old_entries[i];
175 VALUE key = rbimpl_atomic_value_exchange(&old_entry->key, CONCURRENT_SET_MOVED, RBIMPL_ATOMIC_ACQUIRE);
176 RUBY_ASSERT(key != CONCURRENT_SET_MOVED);
177
178 if (key < CONCURRENT_SET_SPECIAL_VALUE_COUNT) continue;
179 if (!RB_SPECIAL_CONST_P(key) && rb_objspace_garbage_object_p(key)) continue;
180
181 VALUE hash = rbimpl_atomic_value_load(&old_entry->hash, RBIMPL_ATOMIC_RELAXED) & CONCURRENT_SET_HASH_MASK;
182 RUBY_ASSERT(hash != 0);
183 RUBY_ASSERT(hash == concurrent_set_hash(old_set, key));
184
185 // Insert key into new_set.
186 struct concurrent_set_probe probe;
187 int idx = concurrent_set_probe_start(&probe, new_set, hash);
188
189 while (true) {
190 struct concurrent_set_entry *entry = &new_set->entries[idx];
191
192 if (entry->hash == CONCURRENT_SET_EMPTY) {
193 RUBY_ASSERT(entry->key == CONCURRENT_SET_EMPTY);
194
195 new_set->size++;
196 RUBY_ASSERT(new_set->size <= new_set->capacity / 2);
197
198 entry->key = key;
199 entry->hash = hash;
200 break;
201 }
202
203 RUBY_ASSERT(entry->key >= CONCURRENT_SET_SPECIAL_VALUE_COUNT);
204 entry->hash |= CONCURRENT_SET_CONTINUATION_BIT;
205 idx = concurrent_set_probe_next(&probe);
206 }
207 }
208
209 rbimpl_atomic_value_store(set_obj_ptr, new_set_obj, RBIMPL_ATOMIC_RELEASE);
210
211 RB_GC_GUARD(old_set_obj);
212}
213
214static void
215concurrent_set_try_resize(VALUE old_set_obj, VALUE *set_obj_ptr)
216{
217 RB_VM_LOCKING() {
218 concurrent_set_try_resize_without_locking(old_set_obj, set_obj_ptr);
219 }
220}
221
222VALUE
223rb_concurrent_set_find(VALUE *set_obj_ptr, VALUE key)
224{
225 RUBY_ASSERT(key >= CONCURRENT_SET_SPECIAL_VALUE_COUNT);
226
227 VALUE set_obj;
228 VALUE hash = 0;
229 struct concurrent_set *set;
230 struct concurrent_set_probe probe;
231 int idx;
232
233 retry:
234 set_obj = rbimpl_atomic_value_load(set_obj_ptr, RBIMPL_ATOMIC_ACQUIRE);
235 RUBY_ASSERT(set_obj);
236 set = RTYPEDDATA_GET_DATA(set_obj);
237
238 if (hash == 0) {
239 // We don't need to recompute the hash on every retry because it should
240 // never change.
241 hash = concurrent_set_hash(set, key);
242 }
243 RUBY_ASSERT(hash == concurrent_set_hash(set, key));
244
245 idx = concurrent_set_probe_start(&probe, set, hash);
246
247 while (true) {
248 struct concurrent_set_entry *entry = &set->entries[idx];
249 VALUE curr_hash_and_flags = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_ACQUIRE);
250 VALUE curr_hash = curr_hash_and_flags & CONCURRENT_SET_HASH_MASK;
251 bool continuation = curr_hash_and_flags & CONCURRENT_SET_CONTINUATION_BIT;
252
253 if (curr_hash_and_flags == CONCURRENT_SET_EMPTY) {
254 return 0;
255 }
256
257 if (curr_hash != hash) {
258 if (!continuation) {
259 return 0;
260 }
261 idx = concurrent_set_probe_next(&probe);
262 continue;
263 }
264
265 VALUE curr_key = rbimpl_atomic_value_load(&entry->key, RBIMPL_ATOMIC_ACQUIRE);
266
267 switch (curr_key) {
268 case CONCURRENT_SET_EMPTY:
269 // In-progress insert: hash written but key not yet
270 break;
271 case CONCURRENT_SET_DELETED:
272 break;
273 case CONCURRENT_SET_MOVED:
274 // Wait
275 RB_VM_LOCKING();
276
277 goto retry;
278 default: {
279 if (UNLIKELY(!RB_SPECIAL_CONST_P(curr_key) && rb_objspace_garbage_object_p(curr_key))) {
280 // This is a weakref set, so after marking but before sweeping is complete we may find a matching garbage object.
281 // Skip it and let the GC pass clean it up
282 break;
283 }
284
285 if (set->funcs->cmp(key, curr_key)) {
286 // We've found a match.
287 RB_GC_GUARD(set_obj);
288 return curr_key;
289 }
290
291 if (!continuation) {
292 return 0;
293 }
294
295 break;
296 }
297 }
298
299 idx = concurrent_set_probe_next(&probe);
300 }
301}
302
303VALUE
304rb_concurrent_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data)
305{
306 RUBY_ASSERT(key >= CONCURRENT_SET_SPECIAL_VALUE_COUNT);
307
308 // First attempt to find
309 {
310 VALUE result = rb_concurrent_set_find(set_obj_ptr, key);
311 if (result) return result;
312 }
313
314 // First time we need to call create, and store the hash
315 VALUE set_obj = rbimpl_atomic_value_load(set_obj_ptr, RBIMPL_ATOMIC_ACQUIRE);
316 RUBY_ASSERT(set_obj);
317
318 struct concurrent_set *set = RTYPEDDATA_GET_DATA(set_obj);
319 key = set->funcs->create(key, data);
320 VALUE hash = concurrent_set_hash(set, key);
321
322 struct concurrent_set_probe probe;
323 int idx;
324
325 goto start_search;
326
327retry:
328 // On retries we only need to load the hash object
329 set_obj = rbimpl_atomic_value_load(set_obj_ptr, RBIMPL_ATOMIC_ACQUIRE);
330 RUBY_ASSERT(set_obj);
331 set = RTYPEDDATA_GET_DATA(set_obj);
332
333 RUBY_ASSERT(hash == concurrent_set_hash(set, key));
334
335start_search:
336 idx = concurrent_set_probe_start(&probe, set, hash);
337
338 while (true) {
339 struct concurrent_set_entry *entry = &set->entries[idx];
340 VALUE curr_hash_and_flags = rbimpl_atomic_value_load(&entry->hash, RBIMPL_ATOMIC_ACQUIRE);
341 VALUE curr_hash = curr_hash_and_flags & CONCURRENT_SET_HASH_MASK;
342 bool continuation = curr_hash_and_flags & CONCURRENT_SET_CONTINUATION_BIT;
343
344 if (curr_hash_and_flags == CONCURRENT_SET_EMPTY) {
345 // Reserve this slot for our hash value
346 curr_hash_and_flags = rbimpl_atomic_value_cas(&entry->hash, CONCURRENT_SET_EMPTY, hash, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED);
347 if (curr_hash_and_flags != CONCURRENT_SET_EMPTY) {
348 // Lost race, retry same slot to check winner's hash
349 continue;
350 }
351
352 // CAS succeeded, so these are the values stored
353 curr_hash_and_flags = hash;
354 curr_hash = hash;
355
356 // Fall through to try to claim key
357 }
358
359 if (curr_hash != hash) {
360 goto probe_next;
361 }
362
363 VALUE curr_key = rbimpl_atomic_value_load(&entry->key, RBIMPL_ATOMIC_ACQUIRE);
364
365 switch (curr_key) {
366 case CONCURRENT_SET_EMPTY: {
367 rb_atomic_t prev_size = rbimpl_atomic_fetch_add(&set->size, 1, RBIMPL_ATOMIC_RELAXED);
368
369 // Load_factor reached at 75% full. ex: prev_size: 32, capacity: 64, load_factor: 50%.
370 bool load_factor_reached = (uint64_t)(prev_size * 4) >= (uint64_t)(set->capacity * 3);
371
372 if (UNLIKELY(load_factor_reached)) {
373 concurrent_set_try_resize(set_obj, set_obj_ptr);
374 goto retry;
375 }
376
377 VALUE prev_key = rbimpl_atomic_value_cas(&entry->key, CONCURRENT_SET_EMPTY, key, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED);
378 if (prev_key == CONCURRENT_SET_EMPTY) {
379 RUBY_ASSERT(rb_concurrent_set_find(set_obj_ptr, key) == key);
380 RB_GC_GUARD(set_obj);
381 return key;
382 }
383 else {
384 // Entry was not inserted.
385 rbimpl_atomic_sub(&set->size, 1, RBIMPL_ATOMIC_RELAXED);
386
387 // Another thread won the race, try again at the same location.
388 continue;
389 }
390 }
391 case CONCURRENT_SET_DELETED:
392 break;
393 case CONCURRENT_SET_MOVED:
394 // Wait
395 RB_VM_LOCKING();
396 goto retry;
397 default:
398 // We're never GC during our search
399 // If the continuation bit wasn't set at the start of our search,
400 // any concurrent find with the same hash value would also look at
401 // this location and try to swap curr_key
402 if (UNLIKELY(!RB_SPECIAL_CONST_P(curr_key) && rb_objspace_garbage_object_p(curr_key))) {
403 if (continuation) {
404 goto probe_next;
405 }
406 {
407 VALUE prev = rbimpl_atomic_value_cas(&entry->key, curr_key, CONCURRENT_SET_EMPTY, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED);
408 if (prev == curr_key) {
409 rbimpl_atomic_sub(&set->size, 1, RBIMPL_ATOMIC_RELAXED);
410 }
411 }
412 continue;
413 }
414
415 if (set->funcs->cmp(key, curr_key)) {
416 // We've found a live match.
417 RB_GC_GUARD(set_obj);
418
419 // We created key using set->funcs->create, but we didn't end
420 // up inserting it into the set. Free it here to prevent memory
421 // leaks.
422 if (set->funcs->free) set->funcs->free(key);
423
424 return curr_key;
425 }
426 break;
427 }
428
429 probe_next:
430 RUBY_ASSERT(curr_hash_and_flags != CONCURRENT_SET_EMPTY);
431 concurrent_set_mark_continuation(entry, curr_hash_and_flags);
432 idx = concurrent_set_probe_next(&probe);
433 }
434}
435
436static void
437concurrent_set_delete_entry_locked(struct concurrent_set *set, struct concurrent_set_entry *entry)
438{
439 ASSERT_vm_locking_with_barrier();
440
441 if (entry->hash & CONCURRENT_SET_CONTINUATION_BIT) {
442 entry->hash = CONCURRENT_SET_CONTINUATION_BIT;
443 entry->key = CONCURRENT_SET_DELETED;
444 set->deleted_entries++;
445 }
446 else {
447 entry->hash = CONCURRENT_SET_EMPTY;
448 entry->key = CONCURRENT_SET_EMPTY;
449 set->size--;
450 }
451}
452
453VALUE
454rb_concurrent_set_delete_by_identity(VALUE set_obj, VALUE key)
455{
456 ASSERT_vm_locking_with_barrier();
457
458 struct concurrent_set *set = RTYPEDDATA_GET_DATA(set_obj);
459
460 VALUE hash = concurrent_set_hash(set, key);
461
462 struct concurrent_set_probe probe;
463 int idx = concurrent_set_probe_start(&probe, set, hash);
464
465 while (true) {
466 struct concurrent_set_entry *entry = &set->entries[idx];
467 VALUE curr_key = entry->key;
468
469 switch (curr_key) {
470 case CONCURRENT_SET_EMPTY:
471 // We didn't find our entry to delete.
472 return 0;
473 case CONCURRENT_SET_DELETED:
474 break;
475 case CONCURRENT_SET_MOVED:
476 rb_bug("rb_concurrent_set_delete_by_identity: moved entry");
477 break;
478 default:
479 if (key == curr_key) {
480 RUBY_ASSERT((entry->hash & CONCURRENT_SET_HASH_MASK) == hash);
481 concurrent_set_delete_entry_locked(set, entry);
482 return curr_key;
483 }
484 break;
485 }
486
487 idx = concurrent_set_probe_next(&probe);
488 }
489}
490
491void
492rb_concurrent_set_foreach_with_replace(VALUE set_obj, int (*callback)(VALUE *key, void *data), void *data)
493{
494 ASSERT_vm_locking_with_barrier();
495
496 struct concurrent_set *set = RTYPEDDATA_GET_DATA(set_obj);
497
498 for (unsigned int i = 0; i < set->capacity; i++) {
499 struct concurrent_set_entry *entry = &set->entries[i];
500 VALUE key = entry->key;
501
502 switch (key) {
503 case CONCURRENT_SET_EMPTY:
504 case CONCURRENT_SET_DELETED:
505 continue;
506 case CONCURRENT_SET_MOVED:
507 rb_bug("rb_concurrent_set_foreach_with_replace: moved entry");
508 break;
509 default: {
510 int ret = callback(&entry->key, data);
511 switch (ret) {
512 case ST_STOP:
513 return;
514 case ST_DELETE:
515 concurrent_set_delete_entry_locked(set, entry);
516 break;
517 }
518 break;
519 }
520 }
521 }
522}
#define RUBY_ASSERT(...)
Asserts that the given expression is truthy if and only if RUBY_DEBUG is truthy.
Definition assert.h:219
Atomic operations.
std::atomic< unsigned > rb_atomic_t
Type that is eligible for atomic operations.
Definition atomic.h:69
#define RUBY_ATOMIC_LOAD(var)
Atomic load.
Definition atomic.h:175
#define ZALLOC_N
Old name of RB_ZALLOC_N.
Definition memory.h:401
#define RB_GC_GUARD(v)
Prevents premature destruction of local objects.
Definition memory.h:167
#define TypedData_Make_Struct(klass, type, data_type, sval)
Identical to TypedData_Wrap_Struct, except it allocates a new data region internally instead of takin...
Definition rtypeddata.h:604
static bool RB_SPECIAL_CONST_P(VALUE obj)
Checks if the given object is of enum ruby_special_consts.
This is the struct that holds necessary info for a struct.
Definition rtypeddata.h:238
const char * wrap_struct_name
Name of structs of this kind.
Definition rtypeddata.h:245
uintptr_t VALUE
Type that represents a Ruby object.
Definition value.h:40